Coverage for Doctrine_Connection_UnitOfWork

Back to coverage report

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