UnitOfWork.php 12.6 KB
Newer Older
1
<?php
lsmith's avatar
lsmith committed
2
/*
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21
Doctrine::autoload('Doctrine_Connection_Module');
22
/**
zYne's avatar
zYne committed
23
 * Doctrine_Connection_UnitOfWork
24
 *
25 26 27 28 29 30 31 32
 * @package     Doctrine
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @category    Object Relational Mapping
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 */
zYne's avatar
zYne committed
33
class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
lsmith's avatar
lsmith committed
34
{
35 36 37
    /**
     * buildFlushTree
     * builds a flush tree that is used in transactions
lsmith's avatar
lsmith committed
38
     *
39
     * The returned array has all the initialized components in
lsmith's avatar
lsmith committed
40
     * 'correct' order. Basically this means that the records of those
41
     * components can be saved safely in the order specified by the returned array.
42
     *
43
     * @param array $tables
44 45
     * @return array
     */
lsmith's avatar
lsmith committed
46 47
    public function buildFlushTree(array $tables)
    {
48
        $tree = array();
lsmith's avatar
lsmith committed
49
        foreach ($tables as $k => $table) {
zYne's avatar
zYne committed
50

lsmith's avatar
lsmith committed
51
            if ( ! ($table instanceof Doctrine_Table)) {
52
                $table = $this->conn->getTable($table, false);
lsmith's avatar
lsmith committed
53
            }
54 55
            $nm     = $table->getComponentName();

zYne's avatar
zYne committed
56
            $index  = array_search($nm, $tree);
57

lsmith's avatar
lsmith committed
58
            if ($index === false) {
59 60 61 62 63
                $tree[] = $nm;
                $index  = max(array_keys($tree));
            }

            $rels = $table->getRelations();
lsmith's avatar
lsmith committed
64

65
            // group relations
lsmith's avatar
lsmith committed
66 67 68

            foreach ($rels as $key => $rel) {
                if ($rel instanceof Doctrine_Relation_ForeignKey) {
69 70 71 72 73
                    unset($rels[$key]);
                    array_unshift($rels, $rel);
                }
            }

lsmith's avatar
lsmith committed
74
            foreach ($rels as $rel) {
75 76 77 78 79
                $name   = $rel->getTable()->getComponentName();
                $index2 = array_search($name,$tree);
                $type   = $rel->getType();

                // skip self-referenced relations
zYne's avatar
zYne committed
80
                if ($name === $nm) {
81
                    continue;
82
                }
83

lsmith's avatar
lsmith committed
84 85 86
                if ($rel instanceof Doctrine_Relation_ForeignKey) {
                    if ($index2 !== false) {
                        if ($index2 >= $index)
87 88 89 90 91 92 93 94 95
                            continue;

                        unset($tree[$index]);
                        array_splice($tree,$index2,0,$nm);
                        $index = $index2;
                    } else {
                        $tree[] = $name;
                    }

lsmith's avatar
lsmith committed
96 97 98
                } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
                    if ($index2 !== false) {
                        if ($index2 <= $index)
99 100 101 102 103 104 105 106
                            continue;

                        unset($tree[$index2]);
                        array_splice($tree,$index,0,$name);
                    } else {
                        array_unshift($tree,$name);
                        $index++;
                    }
lsmith's avatar
lsmith committed
107
                } elseif ($rel instanceof Doctrine_Relation_Association) {
108 109
                    $t = $rel->getAssociationFactory();
                    $n = $t->getComponentName();
lsmith's avatar
lsmith committed
110 111

                    if ($index2 !== false)
112
                        unset($tree[$index2]);
lsmith's avatar
lsmith committed
113

zYne's avatar
zYne committed
114
                    array_splice($tree, $index, 0, $name);
115 116
                    $index++;

zYne's avatar
zYne committed
117
                    $index3 = array_search($n, $tree);
118

lsmith's avatar
lsmith committed
119 120
                    if ($index3 !== false) {
                        if ($index3 >= $index)
121 122 123
                            continue;

                        unset($tree[$index]);
zYne's avatar
zYne committed
124
                        array_splice($tree, $index3, 0, $n);
125 126 127 128 129 130 131 132 133
                        $index = $index2;
                    } else {
                        $tree[] = $n;
                    }
                }
            }
        }
        return array_values($tree);
    }
134 135 136 137
    /**
     * saveRelated
     * saves all related records to $record
     *
zYne's avatar
zYne committed
138
     * @throws PDOException         if something went wrong at database level
139 140
     * @param Doctrine_Record $record
     */
lsmith's avatar
lsmith committed
141 142
    public function saveRelated(Doctrine_Record $record)
    {
143
        $saveLater = array();
zYne's avatar
zYne committed
144
        foreach ($record->getReferences() as $k => $v) {
145
            $fk = $record->getTable()->getRelation($k);
lsmith's avatar
lsmith committed
146
            if ($fk instanceof Doctrine_Relation_ForeignKey ||
zYne's avatar
zYne committed
147
                $fk instanceof Doctrine_Relation_LocalKey) {
148 149 150 151 152 153
                $local = $fk->getLocal();
                $foreign = $fk->getForeign();

                if ($record->getTable()->hasPrimaryKey($fk->getLocal())) {
                    if ( ! $record->exists()) {
                        $saveLater[$k] = $fk;
154
                    } else {
155
                        $v->save($this->conn);
156 157 158 159
                    }
                } else {
                    // ONE-TO-ONE relationship
                    $obj = $record->get($fk->getAlias());
160

zYne's avatar
zYne committed
161
                    if ($obj->exists()) {
162
                        $obj->save($this->conn);
163 164
                    }
                }
165

166 167 168 169 170 171
            }
        }
        return $saveLater;
    }
    /**
     * saveAssociations
lsmith's avatar
lsmith committed
172 173
     *
     * this method takes a diff of one-to-many / many-to-many original and
174 175 176 177 178 179 180
     * current collections and applies the changes
     *
     * for example if original many-to-many related collection has records with
     * primary keys 1,2 and 3 and the new collection has records with primary keys
     * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
     * save new associations to 4 and 5
     *
zYne's avatar
zYne committed
181
     * @throws PDOException         if something went wrong at database level
182 183 184
     * @param Doctrine_Record $record
     * @return void
     */
lsmith's avatar
lsmith committed
185 186
    public function saveAssociations(Doctrine_Record $record)
    {
zYne's avatar
zYne committed
187 188
        foreach ($record->getReferences() as $k => $v) {
            $rel = $record->getTable()->getRelation($k);
zYne's avatar
zYne committed
189
            
zYne's avatar
zYne committed
190
            if ($rel instanceof Doctrine_Relation_Association) {   
zYne's avatar
zYne committed
191
                $v->save($this->conn);
zYne's avatar
zYne committed
192

zYne's avatar
zYne committed
193 194 195 196 197 198
                $assocTable = $rel->getAssociationTable();
                foreach ($v->getDeleteDiff() as $r) {
                    $query = 'DELETE FROM ' . $assocTable->getTableName()
                           . ' WHERE ' . $rel->getForeign() . ' = ?'
                           . ' AND ' . $rel->getLocal() . ' = ?';

zYne's avatar
zYne committed
199
                    $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
zYne's avatar
zYne committed
200 201 202 203 204 205 206 207
                }
                foreach ($v->getInsertDiff() as $r) {
                    $assocRecord = $assocTable->create();
                    $assocRecord->set($rel->getForeign(), $r);
                    $assocRecord->set($rel->getLocal(), $record);
                    $assocRecord->save($this->conn);
                }
            }
208 209 210 211 212 213
        }
    }
    /**
     * deletes all related composites
     * this method is always called internally when a record is deleted
     *
zYne's avatar
zYne committed
214
     * @throws PDOException         if something went wrong at database level
215 216
     * @return void
     */
lsmith's avatar
lsmith committed
217 218
    public function deleteComposites(Doctrine_Record $record)
    {
lsmith's avatar
lsmith committed
219 220
        foreach ($record->getTable()->getRelations() as $fk) {
            switch ($fk->getType()) {
221 222 223
                case Doctrine_Relation::ONE_COMPOSITE:
                case Doctrine_Relation::MANY_COMPOSITE:
                    $obj = $record->get($fk->getAlias());
224
                    $obj->delete($this->conn);
225
                    break;
zYne's avatar
zYne committed
226
            }
227 228
        }
    }
zYne's avatar
zYne committed
229
    /**
lsmith's avatar
lsmith committed
230
     * saveAll
zYne's avatar
zYne committed
231 232 233 234 235
     * persists all the pending records from all tables
     *
     * @throws PDOException         if something went wrong at database level
     * @return void
     */
lsmith's avatar
lsmith committed
236 237
    public function saveAll()
    {
zYne's avatar
zYne committed
238 239 240 241
        // get the flush tree
        $tree = $this->buildFlushTree($this->conn->getTables());

        // save all records
lsmith's avatar
lsmith committed
242
        foreach ($tree as $name) {
zYne's avatar
zYne committed
243 244
            $table = $this->conn->getTable($name);

lsmith's avatar
lsmith committed
245
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
246 247 248
                $this->conn->save($record);
            }
        }
lsmith's avatar
lsmith committed
249

zYne's avatar
zYne committed
250
        // save all associations
lsmith's avatar
lsmith committed
251
        foreach ($tree as $name) {
zYne's avatar
zYne committed
252 253
            $table = $this->conn->getTable($name);

lsmith's avatar
lsmith committed
254
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
255 256 257 258
                $this->saveAssociations($record);
            }
        }
    }
259
    /**
260
     * update
261 262
     * updates the given record
     *
263 264
     * @param Doctrine_Record $record   record to be updated
     * @return boolean                  whether or not the update was successful
265
     */
lsmith's avatar
lsmith committed
266 267
    public function update(Doctrine_Record $record)
    {
268 269 270 271
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreUpdate($record);

        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
272
        if (empty($array)) {
273
            return false;
lsmith's avatar
lsmith committed
274
        }
275
        $set   = array();
lsmith's avatar
lsmith committed
276
        foreach ($array as $name => $value) {
277
            $set[] = $name . ' = ?';
278

279 280 281 282 283 284 285 286
            if ($value instanceof Doctrine_Record) {
                switch ($value->state()) {
                    case Doctrine_Record::STATE_TCLEAN:
                    case Doctrine_Record::STATE_TDIRTY:
                        $record->save($this->conn);
                    default:
                        $array[$name] = $value->getIncremented();
                        $record->set($name, $value->getIncremented());
287
                }
288 289
            }
        }
290 291 292 293

        $params   = array_values($array);
        $id       = $record->obtainIdentifier();

lsmith's avatar
lsmith committed
294
        if ( ! is_array($id)) {
295
            $id = array($id);
lsmith's avatar
lsmith committed
296
        }
297 298 299
        $id     = array_values($id);
        $params = array_merge($params, $id);

300
        $sql  = 'UPDATE ' . $this->conn->quoteIdentifier($record->getTable()->getTableName())
lsmith's avatar
lsmith committed
301 302
              . ' SET ' . implode(', ', $set)
              . ' WHERE ' . implode(' = ? AND ', $record->getTable()->getPrimaryKeys())
zYne's avatar
zYne committed
303
              . ' = ?';
304

305
        $stmt = $this->conn->getDbh()->prepare($sql);
306 307 308 309 310 311 312 313 314 315 316 317 318 319
        $stmt->execute($params);

        $record->assignIdentifier(true);

        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onUpdate($record);

        return true;
    }
    /**
     * inserts a record into database
     *
     * @param Doctrine_Record $record   record to be inserted
     * @return boolean
     */
lsmith's avatar
lsmith committed
320 321
    public function insert(Doctrine_Record $record)
    {
322 323
         // listen the onPreInsert event
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreInsert($record);
lsmith's avatar
lsmith committed
324

325 326
        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
327
        if (empty($array)) {
328
            return false;
lsmith's avatar
lsmith committed
329
        }
330 331 332
        $table     = $record->getTable();
        $keys      = $table->getPrimaryKeys();

zYne's avatar
zYne committed
333
        $seq       = $record->getTable()->sequenceName;
334

lsmith's avatar
lsmith committed
335
        if ( ! empty($seq)) {
336
            $id             = $this->conn->sequence->nextId($seq);
337 338
            $name           = $record->getTable()->getIdentifier();
            $array[$name]   = $id;
339

zYne's avatar
zYne committed
340
            $record->assignIdentifier($id);
341 342 343 344
        }

        $this->conn->insert($table->getTableName(), $array);

zYne's avatar
zYne committed
345 346
        if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
            $table->getIdentifierType() != Doctrine_Identifier::NORMAL) {
347 348

            if (strtolower($this->conn->getName()) == 'pgsql') {
zYne's avatar
zYne committed
349
                $seq = $table->getTableName() . '_' . $keys[0];
350 351 352
            }

            $id = $this->conn->sequence->lastInsertId($seq);
353

zYne's avatar
zYne committed
354
            if ( ! $id) {
355
                $id = $table->getMaxIdentifier();
zYne's avatar
zYne committed
356
            }
lsmith's avatar
lsmith committed
357

358
            $record->assignIdentifier($id);
lsmith's avatar
lsmith committed
359
        } else {
360
            $record->assignIdentifier(true);
lsmith's avatar
lsmith committed
361
        }
362 363 364 365 366 367

        // listen the onInsert event
        $table->getAttribute(Doctrine::ATTR_LISTENER)->onInsert($record);

        return true;
    }
368
}