UnitOfWork.php 11.9 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) {
147 148 149 150 151 152
                $local = $fk->getLocal();
                $foreign = $fk->getForeign();

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

zYne's avatar
zYne committed
160
                    if ($obj->state() != Doctrine_Record::STATE_TCLEAN) {
161
                        $obj->save();
162 163
                    }
                }
164

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

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

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

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

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

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

        $array = $record->getPrepared();

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

lsmith's avatar
lsmith committed
263
                if ($value instanceof Doctrine_Record) {
zYne's avatar
zYne committed
264
                    switch ($value->state()) {
265 266 267 268 269 270
                        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
271
                    };
272
                }
lsmith's avatar
lsmith committed
273
        };
274 275 276 277

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

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

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

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

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

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

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

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

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

zYne's avatar
zYne committed
329 330
        if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
            $table->getIdentifierType() != Doctrine_Identifier::NORMAL) {
331 332

            if (strtolower($this->conn->getName()) == 'pgsql') {
zYne's avatar
zYne committed
333
                $seq = $table->getTableName() . '_' . $keys[0];
334 335 336
            }

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

zYne's avatar
zYne committed
338
            if ( ! $id) {
339
                $id = $table->getMaxIdentifier();
zYne's avatar
zYne committed
340
            }
lsmith's avatar
lsmith committed
341

342
            $record->assignIdentifier($id);
lsmith's avatar
lsmith committed
343
        } else {
344
            $record->assignIdentifier(true);
lsmith's avatar
lsmith committed
345
        }
346 347 348 349 350 351

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

        return true;
    }
lsmith's avatar
lsmith committed
352 353
    public function getIterator()
    { }
354

lsmith's avatar
lsmith committed
355 356
    public function count()
    { }
357
}