Coverage for Doctrine_Connection_UnitOfWork

Back to coverage report

1 <?php
2 /*
3  *  $Id: UnitOfWork.php 2749 2007-10-06 19:10:14Z zYne $
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: 2749 $
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      * saves the given record
136      *
137      * @param Doctrine_Record $record
138      * @return void
139      */
140     public function saveGraph(Doctrine_Record $record)
141     {
142         $conn = $this->getConnection();
143
144         $state = $record->state();
145         if ($state === Doctrine_Record::STATE_LOCKED) {
146             return false;
147         }
148
149         $record->state(Doctrine_Record::STATE_LOCKED);
150
151         $conn->beginTransaction();
152
153         $saveLater = $this->saveRelated($record);
154
155         $record->state($state);
156
157         if ($record->isValid()) {
158             $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
159
160             $record->preSave($event);
161     
162             $record->getTable()->getRecordListener()->preSave($event);
163
164             if ( ! $event->skipOperation) {
165                 switch ($state) {
166                     case Doctrine_Record::STATE_TDIRTY:
167                         $this->insert($record);
168                         break;
169                     case Doctrine_Record::STATE_DIRTY:
170                     case Doctrine_Record::STATE_PROXY:
171                         $this->update($record);
172                         break;
173                     case Doctrine_Record::STATE_CLEAN:
174                     case Doctrine_Record::STATE_TCLEAN:
175
176                         break;
177                 }
178             }
179
180             $record->getTable()->getRecordListener()->postSave($event);
181             
182             $record->postSave($event);
183         } else {
184             $conn->transaction->addInvalid($record);
185         }
186
187         $state = $record->state();
188
189         $record->state(Doctrine_Record::STATE_LOCKED);
190
191         foreach ($saveLater as $fk) {
192             $alias = $fk->getAlias();
193
194             if ($record->hasReference($alias)) {
195                 $obj = $record->$alias;
196                 
197                 // check that the related object is not an instance of Doctrine_Null
198                 if ( ! ($obj instanceof Doctrine_Null)) {
199                     $obj->save($conn);
200                 }
201             }
202         }
203
204         // save the MANY-TO-MANY associations
205         $this->saveAssociations($record);
206
207         $record->state($state);
208
209         $conn->commit();
210
211         return true;
212     }
213     /**
214      * saves the given record
215      *
216      * @param Doctrine_Record $record
217      * @return void
218      */
219     public function save(Doctrine_Record $record)
220     {
221         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
222
223         $record->preSave($event);
224
225         $record->getTable()->getRecordListener()->preSave($event);
226
227         if ( ! $event->skipOperation) {
228             switch ($record->state()) {
229                 case Doctrine_Record::STATE_TDIRTY:
230                     $this->insert($record);
231                     break;
232                 case Doctrine_Record::STATE_DIRTY:
233                 case Doctrine_Record::STATE_PROXY:
234                     $this->update($record);
235                     break;
236                 case Doctrine_Record::STATE_CLEAN:
237                 case Doctrine_Record::STATE_TCLEAN:
238                     // do nothing
239                     break;
240             }
241         }
242
243         $record->getTable()->getRecordListener()->postSave($event);
244         
245         $record->postSave($event);
246     }
247     /**
248      * deletes given record and all the related composites
249      * this operation is isolated by a transaction
250      *
251      * this event can be listened by the onPreDelete and onDelete listeners
252      *
253      * @return boolean      true on success, false on failure
254      */
255     public function delete(Doctrine_Record $record)
256     {
257         if ( ! $record->exists()) {
258             return false;
259         }
260         $this->conn->beginTransaction();
261
262         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
263
264         $record->preDelete($event);
265         
266         $record->getTable()->getRecordListener()->preDelete($event);
267
268         $state = $record->state();
269
270         $record->state(Doctrine_Record::STATE_LOCKED);
271
272         $this->deleteComposites($record);
273
274         if ( ! $event->skipOperation) {
275             $record->state(Doctrine_Record::STATE_TDIRTY);
276
277             $this->deleteRecord($record);
278
279             $record->state(Doctrine_Record::STATE_TCLEAN);
280         } else {
281             // return to original state   
282             $record->state($state);
283         }
284
285
286         $record->getTable()->getRecordListener()->postDelete($event);
287
288         $record->postDelete($event);
289
290         $this->conn->commit();
291
292         return true;
293     }
294     
295     public function deleteRecord(Doctrine_Record $record)
296     {
297         $ids = $record->identifier();
298         $tmp = array();
299         
300         foreach (array_keys($ids) as $id) {
301             $tmp[] = $id . ' = ? ';
302         }
303         
304         $params = array_values($ids);
305
306         $query = 'DELETE FROM '
307                . $this->conn->quoteIdentifier($record->getTable()->getTableName())
308                . ' WHERE ' . implode(' AND ', $tmp);
309
310
311         return $this->conn->exec($query, $params);
312     }
313     /**
314      * deleteMultiple
315      * deletes all records from the pending delete list
316      *
317      * @return void
318      */
319     public function deleteMultiple(array $records)
320     {
321
322         foreach ($this->delete as $name => $deletes) {
323             $record = false;
324             $ids    = array();
325
326             if (is_array($deletes[count($deletes)-1]->getTable()->getIdentifier())) {
327                 if (count($deletes) > 0) {
328                     $query = 'DELETE FROM '
329                            . $this->conn->quoteIdentifier($deletes[0]->getTable()->getTableName())
330                            . ' WHERE ';
331     
332                     $params = array();
333                     $cond = array();
334                     foreach ($deletes as $k => $record) {
335                         $ids = $record->identifier();
336                         $tmp = array();
337                         foreach (array_keys($ids) as $id) {
338                             $tmp[] = $id . ' = ? ';
339                         }
340                         $params = array_merge($params, array_values($ids));
341                         $cond[] = '(' . implode(' AND ', $tmp) . ')';
342                     }
343                     $query .= implode(' OR ', $cond);
344
345                     $this->conn->execute($query, $params);
346                 }
347             } else {
348                 foreach ($deletes as $k => $record) {
349                     $ids[] = $record->getIncremented();
350                 }
351                 if ($record instanceof Doctrine_Record) {
352                     $params = substr(str_repeat('?, ', count($ids)), 0, -2);
353     
354                     $query = 'DELETE FROM '
355                            . $this->conn->quoteIdentifier($record->getTable()->getTableName())
356                            . ' WHERE '
357                            . $record->getTable()->getIdentifier()
358                            . ' IN(' . $params . ')';
359         
360                     $this->conn->execute($query, $ids);
361                 }
362             }
363         }
364     }
365     /**
366      * saveRelated
367      * saves all related records to $record
368      *
369      * @throws PDOException         if something went wrong at database level
370      * @param Doctrine_Record $record
371      */
372     public function saveRelated(Doctrine_Record $record)
373     {
374         $saveLater = array();
375         foreach ($record->getReferences() as $k => $v) {
376             $rel = $record->getTable()->getRelation($k);
377
378             $local = $rel->getLocal();
379             $foreign = $rel->getForeign();
380
381             if ($rel instanceof Doctrine_Relation_ForeignKey) {
382                 $saveLater[$k] = $rel;
383             } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
384                 // ONE-TO-ONE relationship
385                 $obj = $record->get($rel->getAlias());
386
387                 // Protection against infinite function recursion before attempting to save
388                 if ($obj instanceof Doctrine_Record &&
389                     $obj->isModified()) {
390                     $obj->save($this->conn);
391                     /**
392                     $id = array_values($obj->identifier());
393
394                     foreach ((array) $rel->getLocal() as $k => $field) {
395                         $record->set($field, $id[$k]);
396                     }
397                     */
398                 }
399             }
400         }
401
402         return $saveLater;
403     }
404     /**
405      * saveAssociations
406      *
407      * this method takes a diff of one-to-many / many-to-many original and
408      * current collections and applies the changes
409      *
410      * for example if original many-to-many related collection has records with
411      * primary keys 1,2 and 3 and the new collection has records with primary keys
412      * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
413      * save new associations to 4 and 5
414      *
415      * @throws Doctrine_Connection_Exception         if something went wrong at database level
416      * @param Doctrine_Record $record
417      * @return void
418      */
419     public function saveAssociations(Doctrine_Record $record)
420     {
421         foreach ($record->getReferences() as $k => $v) {
422             $rel = $record->getTable()->getRelation($k);
423             
424             if ($rel instanceof Doctrine_Relation_Association) {   
425                 $v->save($this->conn);
426
427                 $assocTable = $rel->getAssociationTable();
428                 foreach ($v->getDeleteDiff() as $r) {
429                     $query = 'DELETE FROM ' . $assocTable->getTableName()
430                            . ' WHERE ' . $rel->getForeign() . ' = ?'
431                            . ' AND ' . $rel->getLocal() . ' = ?';
432
433                     $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
434                 }
435
436                 foreach ($v->getInsertDiff() as $r) {
437                     $assocRecord = $assocTable->create();
438                     $assocRecord->set($rel->getForeign(), $r);
439                     $assocRecord->set($rel->getLocal(), $record);
440
441                     $this->saveGraph($assocRecord);
442                 }
443             }
444         }
445     }
446     /**
447      * deletes all related composites
448      * this method is always called internally when a record is deleted
449      *
450      * @throws PDOException         if something went wrong at database level
451      * @return void
452      */
453     public function deleteComposites(Doctrine_Record $record)
454     {
455         foreach ($record->getTable()->getRelations() as $fk) {
456             if ($fk->isComposite()) {
457                 $obj = $record->get($fk->getAlias());
458                 if ( $obj instanceof Doctrine_Record && 
459                      $obj->state() != Doctrine_Record::STATE_LOCKED)  {
460
461                     $obj->delete($this->conn);
462
463                 }
464             }
465         }
466     }
467     /**
468      * saveAll
469      * persists all the pending records from all tables
470      *
471      * @throws PDOException         if something went wrong at database level
472      * @return void
473      */
474     public function saveAll()
475     {
476         // get the flush tree
477         $tree = $this->buildFlushTree($this->conn->getTables());
478
479         // save all records
480         foreach ($tree as $name) {
481             $table = $this->conn->getTable($name);
482
483             foreach ($table->getRepository() as $record) {
484                 $this->save($record);
485             }
486         }
487
488         // save all associations
489         foreach ($tree as $name) {
490             $table = $this->conn->getTable($name);
491
492             foreach ($table->getRepository() as $record) {
493                 $this->saveAssociations($record);
494             }
495         }
496     }
497     /**
498      * update
499      * updates the given record
500      *
501      * @param Doctrine_Record $record   record to be updated
502      * @return boolean                  whether or not the update was successful
503      */
504     public function update(Doctrine_Record $record)
505     {
506         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_UPDATE);
507
508         $record->preUpdate($event);
509
510         $record->getTable()->getRecordListener()->preUpdate($event);
511
512         if ( ! $event->skipOperation) {
513             $array = $record->getPrepared();
514
515             if (empty($array)) {
516                 return false;
517             }
518             $set = array();
519             foreach ($array as $name => $value) {
520                 if ($value instanceof Doctrine_Expression) {
521                     $set[] = $value->getSql();
522                     unset($array[$name]);
523                 } else {
524
525                     $set[] = $name . ' = ?';
526     
527                     if ($value instanceof Doctrine_Record) {
528                         if ( ! $value->exists()) {
529                             $record->save($this->conn);
530                         }
531                         $array[$name] = $value->getIncremented();
532                         $record->set($name, $value->getIncremented());
533                     }
534                 }
535             }
536
537             $params = array_values($array);
538             $id     = $record->identifier();
539     
540             if ( ! is_array($id)) {
541                 $id = array($id);
542             }
543             $id     = array_values($id);
544             $params = array_merge($params, $id);
545     
546             $sql  = 'UPDATE ' . $this->conn->quoteIdentifier($record->getTable()->getTableName())
547                   . ' SET ' . implode(', ', $set)
548                   . ' WHERE ' . implode(' = ? AND ', (array) $record->getTable()->getIdentifier())
549                   . ' = ?';
550     
551             $stmt = $this->conn->prepare($sql);
552             $stmt->execute($params);
553     
554             $record->assignIdentifier(true);
555         }
556         
557         $record->getTable()->getRecordListener()->postUpdate($event);
558
559         $record->postUpdate($event);
560
561         return true;
562     }
563     /**
564      * inserts a record into database
565      *
566      * @param Doctrine_Record $record   record to be inserted
567      * @return boolean
568      */
569     public function insert(Doctrine_Record $record)
570     {
571          // listen the onPreInsert event
572         $event = new Doctrine_Event($record, Doctrine_Event::RECORD_INSERT);
573
574         $record->preInsert($event);
575         
576         $record->getTable()->getRecordListener()->preInsert($event);
577
578         if ( ! $event->skipOperation) {
579             $array = $record->getPrepared();
580     
581             if (empty($array)) {
582                 return false;
583             }
584             $table     = $record->getTable();
585             $keys      = (array) $table->getIdentifier();
586     
587             $seq       = $record->getTable()->sequenceName;
588     
589             if ( ! empty($seq)) {
590                 $id             = $this->conn->sequence->nextId($seq);
591                 $name           = $record->getTable()->getIdentifier();
592                 $array[$name]   = $id;
593     
594                 $record->assignIdentifier($id);
595             }
596     
597             $this->conn->insert($table->getTableName(), $array);
598     
599             if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
600                 $table->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) {
601     
602                 if (strtolower($this->conn->getName()) == 'pgsql') {
603                     $seq = $table->getTableName() . '_' . $keys[0];
604                 }
605     
606                 $id = $this->conn->sequence->lastInsertId($seq);
607     
608                 if ( ! $id) {
609                     throw new Doctrine_Connection_Exception("Couldn't get last insert identifier.");
610                 }
611     
612                 $record->assignIdentifier($id);
613             } else {
614                 $record->assignIdentifier(true);
615             }
616         }
617         $record->getTable()->addRecord($record);
618
619         $record->getTable()->getRecordListener()->postInsert($event);
620
621         $record->postInsert($event);
622
623         return true;
624     }
625 }