Coverage for Doctrine_Connection_UnitOfWork

Back to coverage report

1 <?php
2 /*
3  *  $Id: UnitOfWork.php 2992 2007-10-22 21:47:05Z phuson $
4  *
5  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
6  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
7  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
8  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
9  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
10  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
11  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
12  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
13  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
14  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
15  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
16  *
17  * This software consists of voluntary contributions made by many individuals
18  * and is licensed under the LGPL. For more information, see
19  * <http://www.phpdoctrine.com>.
20  */
21 Doctrine::autoload('Doctrine_Connection_Module');
22 /**
23  * Doctrine_Connection_UnitOfWork
24  *
25  * @package     Doctrine
26  * @subpackage  Connection
27  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
28  * @link        www.phpdoctrine.com
29  * @since       1.0
30  * @version     $Revision: 2992 $
31  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
32  */
33 class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
34 {
35     /**
36      * buildFlushTree
37      * builds a flush tree that is used in transactions
38      *
39      * The returned array has all the initialized components in
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     an array of Doctrine_Table objects or component names
44      * @return array            an array of component names in flushing order
45      */
46     public function buildFlushTree(array $tables)
47     {
48         $tree = array();
49         foreach ($tables as $k => $table) {
50
51             if ( ! ($table instanceof Doctrine_Table)) {
52                 $table = $this->conn->getTable($table, false);
53             }
54             $nm     = $table->getComponentName();
55
56             $index  = array_search($nm, $tree);
57
58             if ($index === false) {
59                 $tree[] = $nm;
60                 $index  = max(array_keys($tree));
61             }
62
63             $rels = $table->getRelations();
64
65             // group relations
66
67             foreach ($rels as $key => $rel) {
68                 if ($rel instanceof Doctrine_Relation_ForeignKey) {
69                     unset($rels[$key]);
70                     array_unshift($rels, $rel);
71                 }
72             }
73
74             foreach ($rels as $rel) {
75                 $name   = $rel->getTable()->getComponentName();
76                 $index2 = array_search($name,$tree);
77                 $type   = $rel->getType();
78
79                 // skip self-referenced relations
80                 if ($name === $nm) {
81                     continue;
82                 }
83
84                 if ($rel instanceof Doctrine_Relation_ForeignKey) {
85                     if ($index2 !== false) {
86                         if ($index2 >= $index)
87                             continue;
88
89                         unset($tree[$index]);
90                         array_splice($tree,$index2,0,$nm);
91                         $index = $index2;
92                     } else {
93                         $tree[] = $name;
94                     }
95
96                 } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
97                     if ($index2 !== false) {
98                         if ($index2 <= $index)
99                             continue;
100
101                         unset($tree[$index2]);
102                         array_splice($tree,$index,0,$name);
103                     } else {
104                         array_unshift($tree,$name);
105                         $index++;
106                     }
107                 } elseif ($rel instanceof Doctrine_Relation_Association) {
108                     $t = $rel->getAssociationFactory();
109                     $n = $t->getComponentName();
110
111                     if ($index2 !== false)
112                         unset($tree[$index2]);
113
114                     array_splice($tree, $index, 0, $name);
115                     $index++;
116
117                     $index3 = array_search($n, $tree);
118
119                     if ($index3 !== false) {
120                         if ($index3 >= $index)
121                             continue;
122
123                         unset($tree[$index]);
124                         array_splice($tree, $index3, 0, $n);
125                         $index = $index2;
126                     } else {
127                         $tree[] = $n;
128                     }
129                 }
130             }
131         }
132         return array_values($tree);
133     }
134
135     /**
136      * saves the given record
137      *
138      * @param Doctrine_Record $record
139      * @return void
140      */
141     public function saveGraph(Doctrine_Record $record)
142     {
143         $conn = $this->getConnection();
144
145         $state = $record->state();
146         if ($state === Doctrine_Record::STATE_LOCKED) {
147             return false;
148         }
149
150         $record->state(Doctrine_Record::STATE_LOCKED);
151
152         $conn->beginTransaction();
153
154         $saveLater = $this->saveRelated($record);
155
156         $record->state($state);
157
158         if ($record->isValid()) {
159             $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
160
161             $record->preSave($event);
162     
163             $record->getTable()->getRecordListener()->preSave($event);
164             $state = $record->state();
165
166             if ( ! $event->skipOperation) {
167                 switch ($state) {
168                     case Doctrine_Record::STATE_TDIRTY:
169                         $this->insert($record);
170                         break;
171                     case Doctrine_Record::STATE_DIRTY:
172                     case Doctrine_Record::STATE_PROXY:
173                         $this->update($record);
174                         break;
175                     case Doctrine_Record::STATE_CLEAN:
176                     case Doctrine_Record::STATE_TCLEAN:
177
178                         break;
179                 }
180             }
181
182             $record->getTable()->getRecordListener()->postSave($event);
183             
184             $record->postSave($event);
185         } else {
186             $conn->transaction->addInvalid($record);
187         }
188
189         $state = $record->state();
190
191         $record->state(Doctrine_Record::STATE_LOCKED);
192
193         foreach ($saveLater as $fk) {
194             $alias = $fk->getAlias();
195
196             if ($record->hasReference($alias)) {
197                 $obj = $record->$alias;
198                 
199                 // check that the related object is not an instance of Doctrine_Null
200                 if ( ! ($obj instanceof Doctrine_Null)) {
201                     $obj->save($conn);
202                 }
203             }
204         }
205
206         // save the MANY-TO-MANY associations
207         $this->saveAssociations($record);
208
209         $record->state($state);
210
211         $conn->commit();
212
213         return true;
214     }
215
216     /**
217      * saves the given record
218      *
219      * @param Doctrine_Record $record
220      * @return void
221      */
222     public function save(Doctrine_Record $record)
223     {
224         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
225
226         $record->preSave($event);
227
228         $record->getTable()->getRecordListener()->preSave($event);
229
230         if ( ! $event->skipOperation) {
231             switch ($record->state()) {
232                 case Doctrine_Record::STATE_TDIRTY:
233                     $this->insert($record);
234                     break;
235                 case Doctrine_Record::STATE_DIRTY:
236                 case Doctrine_Record::STATE_PROXY:
237                     $this->update($record);
238                     break;
239                 case Doctrine_Record::STATE_CLEAN:
240                 case Doctrine_Record::STATE_TCLEAN:
241                     // do nothing
242                     break;
243             }
244         }
245
246         $record->getTable()->getRecordListener()->postSave($event);
247         
248         $record->postSave($event);
249     }
250
251     /**
252      * deletes given record and all the related composites
253      * this operation is isolated by a transaction
254      *
255      * this event can be listened by the onPreDelete and onDelete listeners
256      *
257      * @return boolean      true on success, false on failure
258      */
259     public function delete(Doctrine_Record $record)
260     {
261         if ( ! $record->exists()) {
262             return false;
263         }
264         $this->conn->beginTransaction();
265
266         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
267
268         $record->preDelete($event);
269         
270         $record->getTable()->getRecordListener()->preDelete($event);
271
272         $state = $record->state();
273
274         $record->state(Doctrine_Record::STATE_LOCKED);
275
276         $this->deleteComposites($record);
277
278         if ( ! $event->skipOperation) {
279             $record->state(Doctrine_Record::STATE_TDIRTY);
280
281             $this->deleteRecord($record);
282
283             $record->state(Doctrine_Record::STATE_TCLEAN);
284         } else {
285             // return to original state   
286             $record->state($state);
287         }
288
289
290         $record->getTable()->getRecordListener()->postDelete($event);
291
292         $record->postDelete($event);
293
294         $this->conn->commit();
295
296         return true;
297     }
298     
299     public function deleteRecord(Doctrine_Record $record)
300     {
301         $ids = $record->identifier();
302         $tmp = array();
303         
304         foreach (array_keys($ids) as $id) {
305             $tmp[] = $id . ' = ? ';
306         }
307         
308         $params = array_values($ids);
309
310         $query = 'DELETE FROM '
311                . $this->conn->quoteIdentifier($record->getTable()->getTableName())
312                . ' WHERE ' . implode(' AND ', $tmp);
313
314
315         return $this->conn->exec($query, $params);
316     }
317
318     /**
319      * deleteMultiple
320      * deletes all records from the pending delete list
321      *
322      * @return void
323      */
324     public function deleteMultiple(array $records)
325     {
326
327         foreach ($this->delete as $name => $deletes) {
328             $record = false;
329             $ids    = array();
330
331             if (is_array($deletes[count($deletes)-1]->getTable()->getIdentifier())) {
332                 if (count($deletes) > 0) {
333                     $query = 'DELETE FROM '
334                            . $this->conn->quoteIdentifier($deletes[0]->getTable()->getTableName())
335                            . ' WHERE ';
336     
337                     $params = array();
338                     $cond = array();
339                     foreach ($deletes as $k => $record) {
340                         $ids = $record->identifier();
341                         $tmp = array();
342                         foreach (array_keys($ids) as $id) {
343                             $tmp[] = $id . ' = ? ';
344                         }
345                         $params = array_merge($params, array_values($ids));
346                         $cond[] = '(' . implode(' AND ', $tmp) . ')';
347                     }
348                     $query .= implode(' OR ', $cond);
349
350                     $this->conn->execute($query, $params);
351                 }
352             } else {
353                 foreach ($deletes as $k => $record) {
354                     $ids[] = $record->getIncremented();
355                 }
356                 if ($record instanceof Doctrine_Record) {
357                     $params = substr(str_repeat('?, ', count($ids)), 0, -2);
358     
359                     $query = 'DELETE FROM '
360                            . $this->conn->quoteIdentifier($record->getTable()->getTableName())
361                            . ' WHERE '
362                            . $record->getTable()->getIdentifier()
363                            . ' IN(' . $params . ')';
364         
365                     $this->conn->execute($query, $ids);
366                 }
367             }
368         }
369     }
370
371     /**
372      * saveRelated
373      * saves all related records to $record
374      *
375      * @throws PDOException         if something went wrong at database level
376      * @param Doctrine_Record $record
377      */
378     public function saveRelated(Doctrine_Record $record)
379     {
380         $saveLater = array();
381         foreach ($record->getReferences() as $k => $v) {
382             $rel = $record->getTable()->getRelation($k);
383
384             $local = $rel->getLocal();
385             $foreign = $rel->getForeign();
386
387             if ($rel instanceof Doctrine_Relation_ForeignKey) {
388                 $saveLater[$k] = $rel;
389             } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
390                 // ONE-TO-ONE relationship
391                 $obj = $record->get($rel->getAlias());
392
393                 // Protection against infinite function recursion before attempting to save
394                 if ($obj instanceof Doctrine_Record &&
395                     $obj->isModified()) {
396                     $obj->save($this->conn);
397                     /**
398                     $id = array_values($obj->identifier());
399
400                     foreach ((array) $rel->getLocal() as $k => $field) {
401                         $record->set($field, $id[$k]);
402                     }
403                     */
404                 }
405             }
406         }
407
408         return $saveLater;
409     }
410
411     /**
412      * saveAssociations
413      *
414      * this method takes a diff of one-to-many / many-to-many original and
415      * current collections and applies the changes
416      *
417      * for example if original many-to-many related collection has records with
418      * primary keys 1,2 and 3 and the new collection has records with primary keys
419      * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
420      * save new associations to 4 and 5
421      *
422      * @throws Doctrine_Connection_Exception         if something went wrong at database level
423      * @param Doctrine_Record $record
424      * @return void
425      */
426     public function saveAssociations(Doctrine_Record $record)
427     {
428         foreach ($record->getReferences() as $k => $v) {
429             $rel = $record->getTable()->getRelation($k);
430             
431             if ($rel instanceof Doctrine_Relation_Association) {   
432                 $v->save($this->conn);
433
434                 $assocTable = $rel->getAssociationTable();
435                 foreach ($v->getDeleteDiff() as $r) {
436                     $query = 'DELETE FROM ' . $assocTable->getTableName()
437                            . ' WHERE ' . $rel->getForeign() . ' = ?'
438                            . ' AND ' . $rel->getLocal() . ' = ?';
439
440                     $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
441                 }
442
443                 foreach ($v->getInsertDiff() as $r) {
444                     $assocRecord = $assocTable->create();
445                     $assocRecord->set($rel->getForeign(), $r);
446                     $assocRecord->set($rel->getLocal(), $record);
447
448                     $this->saveGraph($assocRecord);
449                 }
450             }
451         }
452     }
453
454     /**
455      * deletes all related composites
456      * this method is always called internally when a record is deleted
457      *
458      * @throws PDOException         if something went wrong at database level
459      * @return void
460      */
461     public function deleteComposites(Doctrine_Record $record)
462     {
463         foreach ($record->getTable()->getRelations() as $fk) {
464             if ($fk->isComposite()) {
465                 $obj = $record->get($fk->getAlias());
466                 if ( $obj instanceof Doctrine_Record && 
467                      $obj->state() != Doctrine_Record::STATE_LOCKED)  {
468
469                     $obj->delete($this->conn);
470
471                 }
472             }
473         }
474     }
475
476     /**
477      * saveAll
478      * persists all the pending records from all tables
479      *
480      * @throws PDOException         if something went wrong at database level
481      * @return void
482      */
483     public function saveAll()
484     {
485         // get the flush tree
486         $tree = $this->buildFlushTree($this->conn->getTables());
487
488         // save all records
489         foreach ($tree as $name) {
490             $table = $this->conn->getTable($name);
491
492             foreach ($table->getRepository() as $record) {
493                 $this->save($record);
494             }
495         }
496
497         // save all associations
498         foreach ($tree as $name) {
499             $table = $this->conn->getTable($name);
500
501             foreach ($table->getRepository() as $record) {
502                 $this->saveAssociations($record);
503             }
504         }
505     }
506
507     /**
508      * update
509      * updates the given record
510      *
511      * @param Doctrine_Record $record   record to be updated
512      * @return boolean                  whether or not the update was successful
513      */
514     public function update(Doctrine_Record $record)
515     {
516         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_UPDATE);
517
518         $record->preUpdate($event);
519
520         $record->getTable()->getRecordListener()->preUpdate($event);
521
522         if ( ! $event->skipOperation) {
523             $array = $record->getPrepared();
524
525             if (empty($array)) {
526                 return false;
527             }
528             $set = array();
529             foreach ($array as $name => $value) {
530                 if ($value instanceof Doctrine_Expression) {
531                     $set[] = $name . ' = ' . $value->getSql();
532                     unset($array[$name]);
533                 } else {
534
535                     $set[] = $name . ' = ?';
536     
537                     if ($value instanceof Doctrine_Record) {
538                         if ( ! $value->exists()) {
539                             $record->save($this->conn);
540                         }
541                         $array[$name] = $value->getIncremented();
542                         $record->set($name, $value->getIncremented());
543                     }
544                 }
545             }
546
547             $params = array_values($array);
548             $id     = $record->identifier();
549     
550             if ( ! is_array($id)) {
551                 $id = array($id);
552             }
553             $id     = array_values($id);
554             $params = array_merge($params, $id);
555     
556             $sql  = 'UPDATE ' . $this->conn->quoteIdentifier($record->getTable()->getTableName())
557                   . ' SET ' . implode(', ', $set)
558                   . ' WHERE ' . implode(' = ? AND ', (array) $record->getTable()->getIdentifier())
559                   . ' = ?';
560     
561             $stmt = $this->conn->prepare($sql);
562             $stmt->execute($params);
563     
564             $record->assignIdentifier(true);
565         }
566         
567         $record->getTable()->getRecordListener()->postUpdate($event);
568
569         $record->postUpdate($event);
570
571         return true;
572     }
573
574     /**
575      * inserts a record into database
576      *
577      * @param Doctrine_Record $record   record to be inserted
578      * @return boolean
579      */
580     public function insert(Doctrine_Record $record)
581     {
582          // listen the onPreInsert event
583         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_INSERT);
584
585         $record->preInsert($event);
586         
587         $record->getTable()->getRecordListener()->preInsert($event);
588
589         if ( ! $event->skipOperation) {
590             $array = $record->getPrepared();
591     
592             if (empty($array)) {
593                 return false;
594             }
595             $table     = $record->getTable();
596             $keys      = (array) $table->getIdentifier();
597     
598             $seq       = $record->getTable()->sequenceName;
599     
600             if ( ! empty($seq)) {
601                 $id             = $this->conn->sequence->nextId($seq);
602                 $name           = $record->getTable()->getIdentifier();
603                 $array[$name]   = $id;
604     
605                 $record->assignIdentifier($id);
606             }
607     
608             $this->conn->insert($table->getTableName(), $array);
609     
610             if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
611                 $table->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) {
612     
613                 if (strtolower($this->conn->getName()) == 'pgsql') {
614                     $seq = $table->getTableName() . '_' . $keys[0];
615                 }
616     
617                 $id = $this->conn->sequence->lastInsertId($seq);
618     
619                 if ( ! $id) {
620                     throw new Doctrine_Connection_Exception("Couldn't get last insert identifier.");
621                 }
622     
623                 $record->assignIdentifier($id);
624             } else {
625                 $record->assignIdentifier(true);
626             }
627         }
628         $record->getTable()->addRecord($record);
629
630         $record->getTable()->getRecordListener()->postInsert($event);
631
632         $record->postInsert($event);
633
634         return true;
635     }
636 }