UnitOfWork.php 14.1 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
     *
zYne's avatar
zYne committed
43 44
     * @param array $tables     an array of Doctrine_Table objects or component names
     * @return array            an array of component names in flushing order
45
     */
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);
    }
zYne's avatar
zYne committed
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
    /**
     * saves the given record
     *
     * @param Doctrine_Record $record
     * @return void
     */
    public function save(Doctrine_Record $record)
    {
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreSave($record);

        switch ($record->state()) {
            case Doctrine_Record::STATE_TDIRTY:
                $this->insert($record);
                break;
            case Doctrine_Record::STATE_DIRTY:
            case Doctrine_Record::STATE_PROXY:
                $this->update($record);
                break;
            case Doctrine_Record::STATE_CLEAN:
            case Doctrine_Record::STATE_TCLEAN:
                // do nothing
                break;
        }

        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onSave($record);
    }
    /**
     * deletes this data access object and all the related composites
     * this operation is isolated by a transaction
     *
     * this event can be listened by the onPreDelete and onDelete listeners
     *
     * @return boolean      true on success, false on failure
     */
    public function delete(Doctrine_Record $record)
    {
        if ( ! $record->exists()) {
            return false;
        }
        $this->conn->beginTransaction();

        $record->getTable()->getListener()->onPreDelete($record);

        $this->deleteComposites($record);

        $this->conn->transaction->addDelete($record);

        $record->getTable()->getListener()->onDelete($record);

        $record->state(Doctrine_Record::STATE_TCLEAN);

        $this->conn->commit();

        return true;
    }
189 190 191 192
    /**
     * saveRelated
     * saves all related records to $record
     *
zYne's avatar
zYne committed
193
     * @throws PDOException         if something went wrong at database level
194 195
     * @param Doctrine_Record $record
     */
lsmith's avatar
lsmith committed
196 197
    public function saveRelated(Doctrine_Record $record)
    {
198
        $saveLater = array();
zYne's avatar
zYne committed
199
        foreach ($record->getReferences() as $k => $v) {
zYne's avatar
zYne committed
200
            $rel = $record->getTable()->getRelation($k);
201

zYne's avatar
zYne committed
202 203 204 205 206 207
            if ($rel instanceof Doctrine_Relation_ForeignKey ||
                $rel instanceof Doctrine_Relation_LocalKey) {
                $local = $rel->getLocal();
                $foreign = $rel->getForeign();

                if ($record->getTable()->hasPrimaryKey($rel->getLocal())) {
208
                    if ( ! $record->exists()) {
zYne's avatar
zYne committed
209
                        $saveLater[$k] = $rel;
210
                    } else {
211
                        $v->save($this->conn);
212 213 214
                    }
                } else {
                    // ONE-TO-ONE relationship
zYne's avatar
zYne committed
215 216
                    $obj = $record->get($rel->getAlias());
                    $obj->save($this->conn);
217
                }
218

219 220 221 222 223 224
            }
        }
        return $saveLater;
    }
    /**
     * saveAssociations
lsmith's avatar
lsmith committed
225 226
     *
     * this method takes a diff of one-to-many / many-to-many original and
227 228 229 230 231 232 233
     * 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
234
     * @throws PDOException         if something went wrong at database level
235 236 237
     * @param Doctrine_Record $record
     * @return void
     */
lsmith's avatar
lsmith committed
238 239
    public function saveAssociations(Doctrine_Record $record)
    {
zYne's avatar
zYne committed
240 241
        foreach ($record->getReferences() as $k => $v) {
            $rel = $record->getTable()->getRelation($k);
zYne's avatar
zYne committed
242
            
zYne's avatar
zYne committed
243
            if ($rel instanceof Doctrine_Relation_Association) {   
zYne's avatar
zYne committed
244
                $v->save($this->conn);
zYne's avatar
zYne committed
245

zYne's avatar
zYne committed
246 247 248 249 250 251
                $assocTable = $rel->getAssociationTable();
                foreach ($v->getDeleteDiff() as $r) {
                    $query = 'DELETE FROM ' . $assocTable->getTableName()
                           . ' WHERE ' . $rel->getForeign() . ' = ?'
                           . ' AND ' . $rel->getLocal() . ' = ?';

zYne's avatar
zYne committed
252
                    $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
zYne's avatar
zYne committed
253 254 255 256 257 258 259 260
                }
                foreach ($v->getInsertDiff() as $r) {
                    $assocRecord = $assocTable->create();
                    $assocRecord->set($rel->getForeign(), $r);
                    $assocRecord->set($rel->getLocal(), $record);
                    $assocRecord->save($this->conn);
                }
            }
261 262 263 264 265 266
        }
    }
    /**
     * deletes all related composites
     * this method is always called internally when a record is deleted
     *
zYne's avatar
zYne committed
267
     * @throws PDOException         if something went wrong at database level
268 269
     * @return void
     */
lsmith's avatar
lsmith committed
270 271
    public function deleteComposites(Doctrine_Record $record)
    {
lsmith's avatar
lsmith committed
272 273
        foreach ($record->getTable()->getRelations() as $fk) {
            switch ($fk->getType()) {
274 275 276
                case Doctrine_Relation::ONE_COMPOSITE:
                case Doctrine_Relation::MANY_COMPOSITE:
                    $obj = $record->get($fk->getAlias());
277
                    $obj->delete($this->conn);
278
                    break;
zYne's avatar
zYne committed
279
            }
280 281
        }
    }
zYne's avatar
zYne committed
282
    /**
lsmith's avatar
lsmith committed
283
     * saveAll
zYne's avatar
zYne committed
284 285 286 287 288
     * persists all the pending records from all tables
     *
     * @throws PDOException         if something went wrong at database level
     * @return void
     */
lsmith's avatar
lsmith committed
289 290
    public function saveAll()
    {
zYne's avatar
zYne committed
291 292 293 294
        // get the flush tree
        $tree = $this->buildFlushTree($this->conn->getTables());

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

lsmith's avatar
lsmith committed
298
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
299
                $this->save($record);
zYne's avatar
zYne committed
300 301
            }
        }
lsmith's avatar
lsmith committed
302

zYne's avatar
zYne committed
303
        // save all associations
lsmith's avatar
lsmith committed
304
        foreach ($tree as $name) {
zYne's avatar
zYne committed
305 306
            $table = $this->conn->getTable($name);

lsmith's avatar
lsmith committed
307
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
308 309 310 311
                $this->saveAssociations($record);
            }
        }
    }
312
    /**
313
     * update
314 315
     * updates the given record
     *
316 317
     * @param Doctrine_Record $record   record to be updated
     * @return boolean                  whether or not the update was successful
318
     */
lsmith's avatar
lsmith committed
319 320
    public function update(Doctrine_Record $record)
    {
321 322 323 324
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreUpdate($record);

        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
325
        if (empty($array)) {
326
            return false;
lsmith's avatar
lsmith committed
327
        }
328
        $set   = array();
lsmith's avatar
lsmith committed
329
        foreach ($array as $name => $value) {
330
            $set[] = $name . ' = ?';
331

332
            if ($value instanceof Doctrine_Record) {
zYne's avatar
zYne committed
333 334
                if ( ! $value->exists()) {
                    $record->save($this->conn);
335
                }
zYne's avatar
zYne committed
336 337
                $array[$name] = $value->getIncremented();
                $record->set($name, $value->getIncremented());
338 339
            }
        }
340

zYne's avatar
zYne committed
341 342
        $params = array_values($array);
        $id     = $record->obtainIdentifier();
343

lsmith's avatar
lsmith committed
344
        if ( ! is_array($id)) {
345
            $id = array($id);
lsmith's avatar
lsmith committed
346
        }
347 348 349
        $id     = array_values($id);
        $params = array_merge($params, $id);

350
        $sql  = 'UPDATE ' . $this->conn->quoteIdentifier($record->getTable()->getTableName())
lsmith's avatar
lsmith committed
351 352
              . ' SET ' . implode(', ', $set)
              . ' WHERE ' . implode(' = ? AND ', $record->getTable()->getPrimaryKeys())
zYne's avatar
zYne committed
353
              . ' = ?';
354

355
        $stmt = $this->conn->getDbh()->prepare($sql);
356 357 358 359 360 361 362 363 364 365 366 367 368 369
        $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
370 371
    public function insert(Doctrine_Record $record)
    {
372 373
         // listen the onPreInsert event
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreInsert($record);
lsmith's avatar
lsmith committed
374

375 376
        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
377
        if (empty($array)) {
378
            return false;
lsmith's avatar
lsmith committed
379
        }
380 381 382
        $table     = $record->getTable();
        $keys      = $table->getPrimaryKeys();

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

lsmith's avatar
lsmith committed
385
        if ( ! empty($seq)) {
386
            $id             = $this->conn->sequence->nextId($seq);
387 388
            $name           = $record->getTable()->getIdentifier();
            $array[$name]   = $id;
389

zYne's avatar
zYne committed
390
            $record->assignIdentifier($id);
391 392 393 394
        }

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

zYne's avatar
zYne committed
395 396
        if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
            $table->getIdentifierType() != Doctrine_Identifier::NORMAL) {
397 398

            if (strtolower($this->conn->getName()) == 'pgsql') {
zYne's avatar
zYne committed
399
                $seq = $table->getTableName() . '_' . $keys[0];
400 401 402
            }

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

zYne's avatar
zYne committed
404
            if ( ! $id) {
405
                $id = $table->getMaxIdentifier();
zYne's avatar
zYne committed
406
            }
lsmith's avatar
lsmith committed
407

408
            $record->assignIdentifier($id);
lsmith's avatar
lsmith committed
409
        } else {
410
            $record->assignIdentifier(true);
lsmith's avatar
lsmith committed
411
        }
412 413 414 415 416 417

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

        return true;
    }
418
}