UnitOfWork.php 11.7 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>
 */
lsmith's avatar
lsmith committed
33 34
class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module implements IteratorAggregate, Countable
{
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) {
50
            $k = $k.$table;
lsmith's avatar
lsmith committed
51
            if ( ! ($table instanceof Doctrine_Table)) {
52
                $table = $this->conn->getTable($table);
lsmith's avatar
lsmith committed
53
            }
54 55 56 57
            $nm     = $table->getComponentName();

            $index  = array_search($nm,$tree);

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
lsmith's avatar
lsmith committed
80
                if ($name === $nm)
81 82
                    continue;

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

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

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

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

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

113 114 115 116 117
                    array_splice($tree,$index, 0,$name);
                    $index++;

                    $index3 = array_search($n,$tree);

lsmith's avatar
lsmith committed
118 119
                    if ($index3 !== false) {
                        if ($index3 >= $index)
120 121 122 123 124 125 126 127 128 129 130 131 132
                            continue;

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

lsmith's avatar
lsmith committed
151 152
                    if ($record->getTable()->hasPrimaryKey($fk->getLocal())) {
                        if ( ! $record->exists()) {
153
                            $saveLater[$k] = $fk;
lsmith's avatar
lsmith committed
154
                        } else {
155
                            $v->save();
lsmith's avatar
lsmith committed
156
                        }
157 158 159 160
                    } else {
                        // ONE-TO-ONE relationship
                        $obj = $record->get($fk->getTable()->getComponentName());

lsmith's avatar
lsmith committed
161
                        if ($obj->getState() != Doctrine_Record::STATE_TCLEAN) {
162
                            $obj->save();
lsmith's avatar
lsmith committed
163
                        }
164 165
                    }
                }
lsmith's avatar
lsmith committed
166
            } elseif ($fk instanceof Doctrine_Relation_Association) {
167 168 169 170 171 172 173
                $v->save();
            }
        }
        return $saveLater;
    }
    /**
     * saveAssociations
lsmith's avatar
lsmith committed
174 175
     *
     * this method takes a diff of one-to-many / many-to-many original and
176 177 178 179 180 181 182
     * 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
183
     * @throws PDOException         if something went wrong at database level
184 185 186
     * @param Doctrine_Record $record
     * @return void
     */
lsmith's avatar
lsmith committed
187 188
    public function saveAssociations(Doctrine_Record $record)
    {
lsmith's avatar
lsmith committed
189
        foreach ($record->getTable()->getRelations() as $rel) {
190 191
            $table   = $rel->getTable();
            $alias   = $rel->getAlias();
192

193 194 195 196 197 198 199
            $rel->processDiff($record);
        }
    }
    /**
     * deletes all related composites
     * this method is always called internally when a record is deleted
     *
zYne's avatar
zYne committed
200
     * @throws PDOException         if something went wrong at database level
201 202
     * @return void
     */
lsmith's avatar
lsmith committed
203 204
    public function deleteComposites(Doctrine_Record $record)
    {
lsmith's avatar
lsmith committed
205 206
        foreach ($record->getTable()->getRelations() as $fk) {
            switch ($fk->getType()) {
207 208 209 210 211
                case Doctrine_Relation::ONE_COMPOSITE:
                case Doctrine_Relation::MANY_COMPOSITE:
                    $obj = $record->get($fk->getAlias());
                    $obj->delete();
                    break;
lsmith's avatar
lsmith committed
212
            };
213 214
        }
    }
zYne's avatar
zYne committed
215
    /**
lsmith's avatar
lsmith committed
216
     * saveAll
zYne's avatar
zYne committed
217 218 219 220 221
     * persists all the pending records from all tables
     *
     * @throws PDOException         if something went wrong at database level
     * @return void
     */
lsmith's avatar
lsmith committed
222 223
    public function saveAll()
    {
zYne's avatar
zYne committed
224 225 226 227
        // get the flush tree
        $tree = $this->buildFlushTree($this->conn->getTables());

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

lsmith's avatar
lsmith committed
231
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
232 233 234
                $this->conn->save($record);
            }
        }
lsmith's avatar
lsmith committed
235

zYne's avatar
zYne committed
236
        // save all associations
lsmith's avatar
lsmith committed
237
        foreach ($tree as $name) {
zYne's avatar
zYne committed
238 239
            $table = $this->conn->getTable($name);

lsmith's avatar
lsmith committed
240
            foreach ($table->getRepository() as $record) {
zYne's avatar
zYne committed
241 242 243 244
                $this->saveAssociations($record);
            }
        }
    }
245 246 247 248 249 250
    /**
     * updates the given record
     *
     * @param Doctrine_Record $record
     * @return boolean
     */
lsmith's avatar
lsmith committed
251 252
    public function update(Doctrine_Record $record)
    {
253 254 255 256
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreUpdate($record);

        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
257
        if (empty($array)) {
258
            return false;
lsmith's avatar
lsmith committed
259
        }
260
        $set   = array();
lsmith's avatar
lsmith committed
261
        foreach ($array as $name => $value) {
262 263
                $set[] = $name." = ?";

lsmith's avatar
lsmith committed
264 265
                if ($value instanceof Doctrine_Record) {
                    switch ($value->getState()) {
266 267 268 269 270 271
                        case Doctrine_Record::STATE_TCLEAN:
                        case Doctrine_Record::STATE_TDIRTY:
                            $record->save();
                        default:
                            $array[$name] = $value->getIncremented();
                            $record->set($name, $value->getIncremented());
lsmith's avatar
lsmith committed
272
                    };
273
                }
lsmith's avatar
lsmith committed
274
        };
275 276 277 278

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

lsmith's avatar
lsmith committed
279
        if ( ! is_array($id)) {
280
            $id = array($id);
lsmith's avatar
lsmith committed
281
        }
282 283 284
        $id     = array_values($id);
        $params = array_merge($params, $id);

lsmith's avatar
lsmith committed
285 286 287
        $sql  = 'UPDATE ' . $record->getTable()->getTableName()
              . ' SET ' . implode(', ', $set)
              . ' WHERE ' . implode(' = ? AND ', $record->getTable()->getPrimaryKeys())
zYne's avatar
zYne committed
288
              . ' = ?';
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304

        $stmt = $this->conn->getDBH()->prepare($sql);
        $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
305 306
    public function insert(Doctrine_Record $record)
    {
307 308
         // listen the onPreInsert event
        $record->getTable()->getAttribute(Doctrine::ATTR_LISTENER)->onPreInsert($record);
lsmith's avatar
lsmith committed
309

310 311
        $array = $record->getPrepared();

lsmith's avatar
lsmith committed
312
        if (empty($array)) {
313
            return false;
lsmith's avatar
lsmith committed
314
        }
315 316 317 318 319
        $table     = $record->getTable();
        $keys      = $table->getPrimaryKeys();

        $seq = $record->getTable()->getSequenceName();

lsmith's avatar
lsmith committed
320
        if ( ! empty($seq)) {
321 322 323 324 325 326 327
            $id             = $this->nextId($seq);
            $name           = $record->getTable()->getIdentifier();
            $array[$name]   = $id;
        }

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

lsmith's avatar
lsmith committed
328
        if (count($keys) == 1 && $keys[0] == $table->getIdentifier()) {
329 330
            $id = $this->conn->getDBH()->lastInsertID();

lsmith's avatar
lsmith committed
331
            if ( ! $id)
332
                $id = $table->getMaxIdentifier();
lsmith's avatar
lsmith committed
333

334
            $record->assignIdentifier($id);
lsmith's avatar
lsmith committed
335
        } else {
336
            $record->assignIdentifier(true);
lsmith's avatar
lsmith committed
337
        }
338 339 340 341 342 343

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

        return true;
    }
lsmith's avatar
lsmith committed
344 345
    public function getIterator()
    { }
346

lsmith's avatar
lsmith committed
347 348
    public function count()
    { }
349
}