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

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

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

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

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

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

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

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

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

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

        $array = $record->getPrepared();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return true;
    }
419
}