# Coverage for Doctrine_Connection_UnitOfWork

Back to coverage report

1 | <?php
2 | /*
3 | * $Id: UnitOfWork.php 3195 2007-11-20 14:26:42Z romanb $
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.org>.
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.org
29 | * @since 1.0
30 | * @version $Revision: 3195 $
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 | $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 | $state = $record->state();
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 | $table = $record->getTable();
270 |
271 | $table->getRecordListener()->preDelete($event);
272 |
273 | $state = $record->state();
274 |
275 | $record->state(Doctrine_Record::STATE_LOCKED);
276 |
277 | $this->deleteComposites($record);
278 |
279 | if ( ! $event->skipOperation) {
280 | $record->state(Doctrine_Record::STATE_TDIRTY);
281 | if ($table->getOption('joinedParents')) {
282 |
283 | foreach ($table->getOption('joinedParents') as $parent) {
284 | $parentTable = $table->getConnection()->getTable($parent);
285 |
286 | $this->conn->delete($parentTable, $record->identifier());
287 | }
288 | }
289 | $this->conn->delete($table, $record->identifier());
290 |
291 | $record->state(Doctrine_Record::STATE_TCLEAN);
292 | } else {
293 | // return to original state
294 | $record->state($state);
295 | }
296 |
297 | $table->getRecordListener()->postDelete($event);
298 |
299 | $record->postDelete($event);
300 |
301 | $table->removeRecord($record);
302 |
303 | $this->conn->commit();
304 |
305 | return true;
306 | }
307 |
308 | /**
309 | * @todo Description. See also the todo for deleteMultiple().
310 | */
311 | public function deleteRecord(Doctrine_Record $record)
312 | {
313 | $ids = $record->identifier();
314 | $tmp = array();
315 |
316 | foreach (array_keys($ids) as $id) {
317 | $tmp[] = $id . ' = ? ';
318 | }
319 |
320 | $params = array_values($ids);
321 |
322 | $query = 'DELETE FROM '
323 | . $this->conn->quoteIdentifier($record->getTable()->getTableName())
324 | . ' WHERE ' . implode(' AND ', $tmp);
325 |
326 |
327 | return $this->conn->exec($query, $params);
328 | }
329 |
330 | /**
331 | * deleteMultiple
332 | * deletes all records from the pending delete list
333 | *
334 | * @return void
335 | * @todo Refactor. Maybe move to the Connection class? Sometimes UnitOfWork constructs
336 | * queries itself and sometimes it leaves the sql construction to Connection.
337 | * This should be changed.
338 | */
339 | public function deleteMultiple(array $records)
340 | {
341 | foreach ($this->delete as $name => $deletes) {
342 | $record = false;
343 | $ids = array();
344 |
345 | // Note: Why is the last element's table identifier checked here and then
346 | // the table object from $deletes[0] used???
347 | if (is_array($deletes[count($deletes)-1]->getTable()->getIdentifier()) &&
348 | count($deletes) > 0) {
349 | $table = $deletes[0]->getTable();
350 | $query = 'DELETE FROM '
351 | . $this->conn->quoteIdentifier($table->getTableName())
352 | . ' WHERE ';
353 |
354 | $params = array();
355 | $cond = array();
356 | foreach ($deletes as $k => $record) {
357 | $ids = $record->identifier();
358 | $tmp = array();
359 | foreach (array_keys($ids) as $id) {
360 | $tmp[] = $table->getColumnName($id) . ' = ? ';
361 | }
362 | $params = array_merge($params, array_values($ids));
363 | $cond[] = '(' . implode(' AND ', $tmp) . ')';
364 | }
365 | $query .= implode(' OR ', $cond);
366 |
367 | $this->conn->execute($query, $params);
368 | } else {
369 | foreach ($deletes as $k => $record) {
370 | $ids[] = $record->getIncremented();
371 | }
372 | // looks pretty messy. $record should be already out of scope. ugly php behaviour.
373 | // even the php manual agrees on that and recommends to unset() the last element
374 | // immediately after the loop ends.
375 | $table = $record->getTable();
376 | if ($record instanceof Doctrine_Record) {
377 | $params = substr(str_repeat('?, ', count($ids)), 0, -2);
378 |
379 | $query = 'DELETE FROM '
380 | . $this->conn->quoteIdentifier($record->getTable()->getTableName())
381 | . ' WHERE '
382 | . $table->getColumnName($table->getIdentifier())
383 | . ' IN(' . $params . ')';
384 |
385 | $this->conn->execute($query, $ids);
386 | }
387 | }
388 | }
389 | }
390 |
391 | /**
392 | * saveRelated
393 | * saves all related records to $record
394 | *
395 | * @throws PDOException if something went wrong at database level
396 | * @param Doctrine_Record $record
397 | */
398 | public function saveRelated(Doctrine_Record $record)
399 | {
400 | $saveLater = array();
401 | foreach ($record->getReferences() as $k => $v) {
402 | $rel = $record->getTable()->getRelation($k);
403 |
404 | $local = $rel->getLocal();
405 | $foreign = $rel->getForeign();
406 |
407 | if ($rel instanceof Doctrine_Relation_ForeignKey) {
408 | $saveLater[$k] = $rel;
409 | } else if ($rel instanceof Doctrine_Relation_LocalKey) {
410 | // ONE-TO-ONE relationship
411 | $obj = $record->get($rel->getAlias());
412 |
413 | // Protection against infinite function recursion before attempting to save
414 | if ($obj instanceof Doctrine_Record && $obj->isModified()) {
415 | $obj->save($this->conn);
416 |
417 | /** Can this be removed?
418 | $id = array_values($obj->identifier());
419 |
420 | foreach ((array) $rel->getLocal() as $k => $field) {
421 | $record->set($field, $id[$k]);
422 | }
423 | */
424 | }
425 | }
426 | }
427 |
428 | return $saveLater;
429 | }
430 |
431 | /**
432 | * saveAssociations
433 | *
434 | * this method takes a diff of one-to-many / many-to-many original and
435 | * current collections and applies the changes
436 | *
437 | * for example if original many-to-many related collection has records with
438 | * primary keys 1,2 and 3 and the new collection has records with primary keys
439 | * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
440 | * save new associations to 4 and 5
441 | *
442 | * @throws Doctrine_Connection_Exception if something went wrong at database level
443 | * @param Doctrine_Record $record
444 | * @return void
445 | */
446 | public function saveAssociations(Doctrine_Record $record)
447 | {
448 | foreach ($record->getReferences() as $k => $v) {
449 | $rel = $record->getTable()->getRelation($k);
450 |
451 | if ($rel instanceof Doctrine_Relation_Association) {
452 | $v->save($this->conn);
453 |
454 | $assocTable = $rel->getAssociationTable();
455 | foreach ($v->getDeleteDiff() as $r) {
456 | $query = 'DELETE FROM ' . $assocTable->getTableName()
457 | . ' WHERE ' . $rel->getForeign() . ' = ?'
458 | . ' AND ' . $rel->getLocal() . ' = ?';
459 |
460 | $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
461 | }
462 |
463 | foreach ($v->getInsertDiff() as $r) {
464 | $assocRecord = $assocTable->create();
465 | $assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r);
466 | $assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record);
467 |
468 | $this->saveGraph($assocRecord);
469 | }
470 | }
471 | }
472 | }
473 |
474 | /**
475 | * deletes all related composites
476 | * this method is always called internally when a record is deleted
477 | *
478 | * @throws PDOException if something went wrong at database level
479 | * @return void
480 | */
481 | public function deleteComposites(Doctrine_Record $record)
482 | {
483 | foreach ($record->getTable()->getRelations() as $fk) {
484 | if ($fk->isComposite()) {
485 | $obj = $record->get($fk->getAlias());
486 | if ($obj instanceof Doctrine_Record &&
487 | $obj->state() != Doctrine_Record::STATE_LOCKED) {
488 | $obj->delete($this->conn);
489 | }
490 | }
491 | }
492 | }
493 |
494 | /**
495 | * saveAll
496 | * persists all the pending records from all tables
497 | *
498 | * @throws PDOException if something went wrong at database level
499 | * @return void
500 | */
501 | public function saveAll()
502 | {
503 | // get the flush tree
504 | $tree = $this->buildFlushTree($this->conn->getTables());
505 |
506 | // save all records
507 | foreach ($tree as $name) {
508 | $table = $this->conn->getTable($name);
509 |
510 | foreach ($table->getRepository() as $record) {
511 | $this->save($record);
512 | }
513 | }
514 |
515 | // save all associations
516 | foreach ($tree as $name) {
517 | $table = $this->conn->getTable($name);
518 |
519 | foreach ($table->getRepository() as $record) {
520 | $this->saveAssociations($record);
521 | }
522 | }
523 | }
524 |
525 | /**
526 | * updates given record
527 | *
528 | * @param Doctrine_Record $record record to be updated
529 | * @return boolean whether or not the update was successful
530 | */
531 | public function update(Doctrine_Record $record)
532 | {
533 | $event = new Doctrine_Event($record, Doctrine_Event::RECORD_UPDATE);
534 |
535 | $record->preUpdate($event);
536 |
537 | $table = $record->getTable();
538 |
539 | $table->getRecordListener()->preUpdate($event);
540 |
541 | if ( ! $event->skipOperation) {
542 | $identifier = $record->identifier();
543 |
544 | if ($table->getOption('joinedParents')) {
545 | $dataSet = $this->formatDataSet($record);
546 |
547 | $component = $table->getComponentName();
548 |
549 | $classes = $table->getOption('joinedParents');
550 | $classes[] = $component;
551 |
552 | foreach ($record as $field => $value) {
553 | if ($value instanceof Doctrine_Record) {
554 | if ( ! $value->exists()) {
555 | $value->save();
556 | }
557 | $record->set($field, $value->getIncremented());
558 | }
559 | }
560 |
561 | foreach ($classes as $class) {
562 | $parentTable = $this->conn->getTable($class);
563 |
564 | $this->conn->update($this->conn->getTable($class), $dataSet[$class], $identifier);
565 | }
566 | } else {
567 | $array = $record->getPrepared();
568 |
569 | $this->conn->update($table, $array, $identifier);
570 | }
571 | $record->assignIdentifier(true);
572 | }
573 |
574 | $table->getRecordListener()->postUpdate($event);
575 |
576 | $record->postUpdate($event);
577 |
578 | return true;
579 | }
580 |
581 | /**
582 | * inserts a record into database
583 | *
584 | * @param Doctrine_Record $record record to be inserted
585 | * @return boolean
586 | */
587 | public function insert(Doctrine_Record $record)
588 | {
589 | // listen the onPreInsert event
590 | $event = new Doctrine_Event($record, Doctrine_Event::RECORD_INSERT);
591 |
592 | $record->preInsert($event);
593 |
594 | $table = $record->getTable();
595 |
596 | $table->getRecordListener()->preInsert($event);
597 |
598 | if ( ! $event->skipOperation) {
599 | if ($table->getOption('joinedParents')) {
600 | $dataSet = $this->formatDataSet($record);
601 |
602 | $component = $table->getComponentName();
603 |
604 | $classes = $table->getOption('joinedParents');
605 | $classes[] = $component;
606 |
607 | foreach ($classes as $k => $parent) {
608 | if ($k === 0) {
609 | $rootRecord = new $parent();
610 |
611 | $rootRecord->merge($dataSet[$parent]);
612 |
613 | $this->processSingleInsert($rootRecord);
614 | } else {
615 | foreach ((array) $rootRecord->identifier() as $id => $value) {
616 | $dataSet[$parent][$id] = $value;
617 | }
618 |
619 | $this->conn->insert($this->conn->getTable($parent), $dataSet[$parent]);
620 | }
621 | }
622 | } else {
623 | $this->processSingleInsert($record);
624 | }
625 | }
626 |
627 | $table->addRecord($record);
628 |
629 | $table->getRecordListener()->postInsert($event);
630 |
631 | $record->postInsert($event);
632 |
633 | return true;
634 | }
635 |
636 | /**
637 | * @todo DESCRIBE WHAT THIS METHOD DOES, PLEASE!
638 | */
639 | public function formatDataSet(Doctrine_Record $record)
640 | {
641 | $table = $record->getTable();
642 |
643 | $dataSet = array();
644 |
645 | $component = $table->getComponentName();
646 |
647 | $array = $record->getPrepared();
648 |
649 | foreach ($table->getColumns() as $columnName => $definition) {
650 | $fieldName = $table->getFieldName($columnName);
651 | if (isset($definition['primary']) && $definition['primary']) {
652 | continue;
653 | }
654 |
655 | if (isset($definition['owner'])) {
656 | $dataSet[$definition['owner']][$fieldName] = $array[$fieldName];
657 | } else {
658 | $dataSet[$component][$fieldName] = $array[$fieldName];
659 | }
660 | }
661 |
662 | return $dataSet;
663 | }
664 |
665 | /**
666 | * @todo DESCRIBE WHAT THIS METHOD DOES, PLEASE!
667 | */
668 | public function processSingleInsert(Doctrine_Record $record)
669 | {
670 | $fields = $record->getPrepared();
671 |
672 | if (empty($fields)) {
673 | return false;
674 | }
675 |
676 | $table = $record->getTable();
677 | $identifier = (array) $table->getIdentifier();
678 |
679 | $seq = $record->getTable()->sequenceName;
680 |
681 | if ( ! empty($seq)) {
682 | $id = $this->conn->sequence->nextId($seq);
683 | $seqName = $table->getIdentifier();
684 | $fields[$seqName] = $id;
685 |
686 | $record->assignIdentifier($id);
687 | }
688 |
689 | $this->conn->insert($table, $fields);
690 |
691 | if (empty($seq) && count($identifier) == 1 && $identifier[0] == $table->getIdentifier() &&
692 | $table->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) {
693 |
694 | if (strtolower($this->conn->getName()) == 'pgsql') {
695 | $seq = $table->getTableName() . '_' . $identifier[0];
696 | }
697 |
698 | $id = $this->conn->sequence->lastInsertId($seq);
699 |
700 | if ( ! $id) {
701 | throw new Doctrine_Connection_Exception("Couldn't get last insert identifier.");
702 | }
703 |
704 | $record->assignIdentifier($id);
705 | } else {
706 | $record->assignIdentifier(true);
707 | }
708 | }
709 | }