# Source for file UnitOfWork.php

Documentation is available at UnitOfWork.php

1. <?php
2. /*
3. * $Id: UnitOfWork.php 2197 2007-08-10 20:35:25Z 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. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
27. * @category Object Relational Mapping
28. * @link www.phpdoctrine.com
29. * @since 1.0
30. * @version $Revision: 2197 $
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) {
51. if ( ! ($table instanceof Doctrine_Table)) {
52. $table = $this->conn->getTable($table, false);
53. }
54. $nm = $table->getComponentName();
56. $index = array_search($nm, $tree);
58. if ($index === false) {
59. $tree[] = $nm;
60. $index = max(array_keys($tree));
61. }
63. $rels = $table->getRelations();
65. // group relations
67. foreach ($rels as $key => $rel) {
68. if ($rel instanceof Doctrine_Relation_ForeignKey) {
69. unset($rels[$key]);
70. array_unshift($rels, $rel);
71. }
72. }
74. foreach ($rels as $rel) {
75. $name = $rel->getTable()->getComponentName();
76. $index2 = array_search($name,$tree);
77. $type = $rel->getType();
79. // skip self-referenced relations
80. if ($name === $nm) {
81. continue;
82. }
84. if ($rel instanceof Doctrine_Relation_ForeignKey) {
85. if ($index2 !== false) {
86. if ($index2 >= $index)
87. continue;
89. unset($tree[$index]);
90. array_splice($tree,$index2,0,$nm);
91. $index = $index2;
92. } else {
93. $tree[] = $name;
94. }
96. } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
97. if ($index2 !== false) {
98. if ($index2 <= $index)
99. continue;
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();
111. if ($index2 !== false)
112. unset($tree[$index2]);
114. array_splice($tree, $index, 0, $name);
115. $index++;
117. $index3 = array_search($n, $tree);
119. if ($index3 !== false) {
120. if ($index3 >= $index)
121. continue;
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();
144. $state = $record->state();
145. if ($state === Doctrine_Record::STATE_LOCKED) {
146. return false;
147. }
149. $record->state(Doctrine_Record::STATE_LOCKED);
151. $conn->beginTransaction();
153. $saveLater = $this->saveRelated($record);
155. $record->state($state);
157. if ($record->isValid()) {
158. $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
160. $record->preSave($event);
162. $record->getTable()->getRecordListener()->preSave($event);
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:
176. break;
177. }
178. }
180. $record->getTable()->getRecordListener()->postSave($event);
182. $record->postSave($event);
183. } else {
184. $conn->transaction->addInvalid($record);
185. }
187. $state = $record->state();
189. $record->state(Doctrine_Record::STATE_LOCKED);
191. foreach ($saveLater as $fk) {
192. $alias = $fk->getAlias();
194. if ($record->hasReference($alias)) {
195. $obj = $record->$alias;
196. $obj->save($conn);
197. }
198. }
200. // save the MANY-TO-MANY associations
201. $this->saveAssociations($record);
203. $record->state($state);
205. $conn->commit();
207. return true;
208. }
209. /**
210. * saves the given record
211. *
212. * @param Doctrine_Record $record
213. * @return void
214. */
215. public function save(Doctrine_Record $record)
216. {
217. $event = new Doctrine_Event($record, Doctrine_Event::RECORD_SAVE);
219. $record->preSave($event);
221. $record->getTable()->getRecordListener()->preSave($event);
223. if ( ! $event->skipOperation) {
224. switch ($record->state()) {
225. case Doctrine_Record::STATE_TDIRTY:
226. $this->insert($record);
227. break;
228. case Doctrine_Record::STATE_DIRTY:
229. case Doctrine_Record::STATE_PROXY:
230. $this->update($record);
231. break;
232. case Doctrine_Record::STATE_CLEAN:
233. case Doctrine_Record::STATE_TCLEAN:
234. // do nothing
235. break;
236. }
237. }
239. $record->getTable()->getRecordListener()->postSave($event);
241. $record->postSave($event);
242. }
243. /**
244. * deletes given record and all the related composites
245. * this operation is isolated by a transaction
246. *
247. * this event can be listened by the onPreDelete and onDelete listeners
248. *
249. * @return boolean true on success, false on failure
250. */
251. public function delete(Doctrine_Record $record)
252. {
253. if ( ! $record->exists()) {
254. return false;
255. }
256. $this->conn->beginTransaction();
258. $event = new Doctrine_Event($record, Doctrine_Event::RECORD_DELETE);
260. $record->preDelete($event);
262. $record->getTable()->getRecordListener()->preDelete($event);
264. $record->state(Doctrine_Record::STATE_LOCKED);
266. $this->deleteComposites($record);
268. $record->state(Doctrine_Record::STATE_TDIRTY);
270. if ( ! $event->skipOperation) {
271. $this->conn->transaction->addDelete($record);
273. $record->state(Doctrine_Record::STATE_TCLEAN);
274. }
276. $record->getTable()->getRecordListener()->postDelete($event);
278. $record->postDelete($event);
280. $this->conn->commit();
282. return true;
283. }
285. /**
286. * saveRelated
287. * saves all related records to $record
288. *
289. * @throws PDOException if something went wrong at database level
290. * @param Doctrine_Record $record
291. */
292. public function saveRelated(Doctrine_Record $record)
293. {
294. $saveLater = array();
295. foreach ($record->getReferences() as $k => $v) {
296. $rel = $record->getTable()->getRelation($k);
298. $local = $rel->getLocal();
299. $foreign = $rel->getForeign();
301. if ($rel instanceof Doctrine_Relation_ForeignKey) {
302. $saveLater[$k] = $rel;
303. } elseif ($rel instanceof Doctrine_Relation_LocalKey) {
304. // ONE-TO-ONE relationship
305. $obj = $record->get($rel->getAlias());
307. // Protection against infinite function recursion before attempting to save
308. if ($obj instanceof Doctrine_Record &&
309. $obj->isModified()) {
310. $obj->save($this->conn);
311. }
312. }
313. }
315. return $saveLater;
316. }
317. /**
318. * saveAssociations
319. *
320. * this method takes a diff of one-to-many / many-to-many original and
321. * current collections and applies the changes
322. *
323. * for example if original many-to-many related collection has records with
324. * primary keys 1,2 and 3 and the new collection has records with primary keys
325. * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
326. * save new associations to 4 and 5
327. *
328. * @throws PDOException if something went wrong at database level
329. * @param Doctrine_Record $record
330. * @return void
331. */
332. public function saveAssociations(Doctrine_Record $record)
333. {
334. foreach ($record->getReferences() as $k => $v) {
335. $rel = $record->getTable()->getRelation($k);
337. if ($rel instanceof Doctrine_Relation_Association) {
338. $v->save($this->conn);
340. $assocTable = $rel->getAssociationTable();
341. foreach ($v->getDeleteDiff() as $r) {
342. $query = 'DELETE FROM ' . $assocTable->getTableName()
343. . ' WHERE ' . $rel->getForeign() . ' = ?'
344. . ' AND ' . $rel->getLocal() . ' = ?';
346. $this->conn->execute($query, array($r->getIncremented(), $record->getIncremented()));
347. }
349. foreach ($v->getInsertDiff() as $r) {
350. $assocRecord = $assocTable->create();
351. $assocRecord->set($rel->getForeign(), $r);
352. $assocRecord->set($rel->getLocal(), $record);
354. $this->saveGraph($assocRecord);
355. }
356. }
357. }
358. }
359. /**
360. * deletes all related composites
361. * this method is always called internally when a record is deleted
362. *
363. * @throws PDOException if something went wrong at database level
364. * @return void
365. */
366. public function deleteComposites(Doctrine_Record $record)
367. {
368. foreach ($record->getTable()->getRelations() as $fk) {
369. switch ($fk->getType()) {
370. case Doctrine_Relation::ONE_COMPOSITE:
371. case Doctrine_Relation::MANY_COMPOSITE:
372. $obj = $record->get($fk->getAlias());
373. if ( $obj instanceof Doctrine_Record &&
374. $obj->state() != Doctrine_Record::STATE_LOCKED) {
376. $obj->delete($this->conn);
378. }
379. break;
380. }
381. }
382. }
383. /**
384. * saveAll
385. * persists all the pending records from all tables
386. *
387. * @throws PDOException if something went wrong at database level
388. * @return void
389. */
390. public function saveAll()
391. {
392. // get the flush tree
393. $tree = $this->buildFlushTree($this->conn->getTables());
395. // save all records
396. foreach ($tree as $name) {
397. $table = $this->conn->getTable($name);
399. foreach ($table->getRepository() as $record) {
400. $this->save($record);
401. }
402. }
404. // save all associations
405. foreach ($tree as $name) {
406. $table = $this->conn->getTable($name);
408. foreach ($table->getRepository() as $record) {
409. $this->saveAssociations($record);
410. }
411. }
412. }
413. /**
414. * update
415. * updates the given record
416. *
417. * @param Doctrine_Record $record record to be updated
418. * @return boolean whether or not the update was successful
419. */
420. public function update(Doctrine_Record $record)
421. {
422. $event = new Doctrine_Event($record, Doctrine_Event::RECORD_UPDATE);
424. $record->preUpdate($event);
426. $record->getTable()->getRecordListener()->preUpdate($event);
428. if ( ! $event->skipOperation) {
429. $array = $record->getPrepared();
431. if (empty($array)) {
432. return false;
433. }
434. $set = array();
435. foreach ($array as $name => $value) {
436. if ($value instanceof Doctrine_Expression) {
437. $set[] = $value->getSql();
438. unset($array[$name]);
439. } else {
441. $set[] = $name . ' = ?';
443. if ($value instanceof Doctrine_Record) {
444. if ( ! $value->exists()) {
445. $record->save($this->conn);
446. }
447. $array[$name] = $value->getIncremented();
448. $record->set($name, $value->getIncremented());
449. }
450. }
451. }
453. $params = array_values($array);
454. $id = $record->identifier();
456. if ( ! is_array($id)) {
457. $id = array($id);
458. }
459. $id = array_values($id);
460. $params = array_merge($params, $id);
462. $sql = 'UPDATE ' . $this->conn->quoteIdentifier($record->getTable()->getTableName())
463. . ' SET ' . implode(', ', $set)
464. . ' WHERE ' . implode(' = ? AND ', $record->getTable()->getPrimaryKeys())
465. . ' = ?';
467. $stmt = $this->conn->prepare($sql);
468. $stmt->execute($params);
470. $record->assignIdentifier(true);
471. }
473. $record->getTable()->getRecordListener()->postUpdate($event);
475. $record->postUpdate($event);
477. return true;
478. }
479. /**
480. * inserts a record into database
481. *
482. * @param Doctrine_Record $record record to be inserted
483. * @return boolean
484. */
485. public function insert(Doctrine_Record $record)
486. {
487. // listen the onPreInsert event
488. $event = new Doctrine_Event($record, Doctrine_Event::RECORD_INSERT);
490. $record->preInsert($event);
492. $record->getTable()->getRecordListener()->preInsert($event);
494. if ( ! $event->skipOperation) {
495. $array = $record->getPrepared();
497. if (empty($array)) {
498. return false;
499. }
500. $table = $record->getTable();
501. $keys = $table->getPrimaryKeys();
503. $seq = $record->getTable()->sequenceName;
505. if ( ! empty($seq)) {
506. $id = $this->conn->sequence->nextId($seq);
507. $name = $record->getTable()->getIdentifier();
508. $array[$name] = $id;
510. $record->assignIdentifier($id);
511. }
513. $this->conn->insert($table->getTableName(), $array);
515. if (empty($seq) && count($keys) == 1 && $keys[0] == $table->getIdentifier() &&
516. $table->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) {
518. if (strtolower($this->conn->getName()) == 'pgsql') {
519. $seq = $table->getTableName() . '_' . $keys[0];
520. }
522. $id = $this->conn->sequence->lastInsertId($seq);
524. if ( ! $id) {
525. $id = $table->getMaxIdentifier();
526. }
528. $record->assignIdentifier($id);
529. } else {
530. $record->assignIdentifier(true);
531. }
532. }
533. $record->getTable()->addRecord($record);
535. $record->getTable()->getRecordListener()->postInsert($event);
537. $record->postInsert($event);
539. return true;
540. }
541. }