# Source for file Transaction.php

Documentation is available at Transaction.php

1. <?php
2. /*
3. * $Id: Transaction.php 2268 2007-08-24 21:43:50Z jackbravo $
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_Transaction
24. * Handles transaction savepoint and isolation abstraction
25. *
26. * @author Konsta Vesterinen <kvesteri@cc.hut.fi>
27. * @author Lukas Smith <smith@pooteeweet.org> (PEAR MDB2 library)
28. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
29. * @package Doctrine
30. * @category Object Relational Mapping
31. * @link www.phpdoctrine.com
32. * @since 1.0
33. * @version $Revision: 2268 $
34. */
35. class Doctrine_Transaction extends Doctrine_Connection_Module
36. {
37. /**
38. * Doctrine_Transaction is in sleep state when it has no active transactions
39. */
40. const STATE_SLEEP = 0;
41. /**
42. * Doctrine_Transaction is in active state when it has one active transaction
43. */
44. const STATE_ACTIVE = 1;
45. /**
46. * Doctrine_Transaction is in busy state when it has multiple active transactions
47. */
48. const STATE_BUSY = 2;
49. /**
50. * @var integer $transactionLevel the nesting level of transactions, used by transaction methods
51. */
52. protected $transactionLevel = 0;
53. /**
54. * @var array $invalid an array containing all invalid records within this transaction
55. */
56. protected $invalid = array();
57. /**
58. * @var array $delete two dimensional pending delete list, the records in
59. * this list will be deleted when transaction is committed
60. */
61. protected $delete = array();
62. /**
63. * @var array $savepoints an array containing all savepoints
64. */
65. protected $savePoints = array();
66. /**
67. * @var array $_collections an array of Doctrine_Collection objects that were affected during the Transaction
68. */
69. protected $_collections = array();
71. /**
72. * addCollection
73. * adds a collection in the internal array of collections
74. *
75. * at the end of each commit this array is looped over and
76. * of every collection Doctrine then takes a snapshot in order
77. * to keep the collections up to date with the database
78. *
79. * @param Doctrine_Collection $coll a collection to be added
80. * @return Doctrine_Transaction this object
81. */
82. public function addCollection(Doctrine_Collection $coll)
83. {
84. $this->_collections[] = $coll;
86. return $this;
87. }
88. /**
89. * getState
90. * returns the state of this connection
91. *
92. * @see Doctrine_Connection_Transaction::STATE_* constants
93. * @return integer the connection state
94. */
95. public function getState()
96. {
97. switch ($this->transactionLevel) {
98. case 0:
99. return Doctrine_Transaction::STATE_SLEEP;
100. break;
101. case 1:
102. return Doctrine_Transaction::STATE_ACTIVE;
103. break;
104. default:
105. return Doctrine_Transaction::STATE_BUSY;
106. }
107. }
109. /**
110. * addDelete
111. * adds record into pending delete list
112. *
113. * @param Doctrine_Record $record a record to be added
114. * @return void
115. */
116. public function addDelete(Doctrine_Record $record)
117. {
118. $name = $record->getTable()->getComponentName();
119. $this->delete[$name][] = $record;
120. }
122. /**
123. * addInvalid
124. * adds record into invalid records list
125. *
126. * @param Doctrine_Record $record
127. * @return boolean false if record already existed in invalid records list,
128. * otherwise true
129. */
130. public function addInvalid(Doctrine_Record $record)
131. {
132. if (in_array($record, $this->invalid, true)) {
133. return false;
134. }
135. $this->invalid[] = $record;
136. return true;
137. }
139. /**
140. * returns the pending delete list
141. *
142. * @return array
143. */
144. public function getDeletes()
145. {
146. return $this->delete;
147. }
149. /**
150. * bulkDelete
151. * deletes all records from the pending delete list
152. *
153. * @return void
154. */
155. public function bulkDelete()
156. {
158. foreach ($this->delete as $name => $deletes) {
159. $record = false;
160. $ids = array();
162. if (is_array($deletes[count($deletes)-1]->getTable()->getIdentifier())) {
163. if (count($deletes) > 0) {
164. $query = 'DELETE FROM '
165. . $this->conn->quoteIdentifier($deletes[0]->getTable()->getTableName())
166. . ' WHERE ';
168. $params = array();
169. $cond = array();
170. foreach ($deletes as $k => $record) {
171. $ids = $record->identifier();
172. $tmp = array();
173. foreach (array_keys($ids) as $id){
174. $tmp[] = $id . ' = ? ';
175. }
176. $params = array_merge($params, array_values($ids));
177. $cond[] = '(' . implode(' AND ', $tmp) . ')';
178. }
179. $query .= implode(' OR ', $cond);
181. $this->conn->execute($query, $params);
182. }
183. } else {
184. foreach ($deletes as $k => $record) {
185. $ids[] = $record->getIncremented();
186. }
187. if ($record instanceof Doctrine_Record) {
188. $params = substr(str_repeat('?, ', count($ids)), 0, -2);
190. $query = 'DELETE FROM '
191. . $this->conn->quoteIdentifier($record->getTable()->getTableName())
192. . ' WHERE '
193. . $record->getTable()->getIdentifier()
194. . ' IN(' . $params . ')';
196. $this->conn->execute($query, $ids);
197. }
198. }
200. }
201. $this->delete = array();
202. }
203. /**
204. * getTransactionLevel
205. * get the current transaction nesting level
206. *
207. * @return integer
208. */
209. public function getTransactionLevel()
210. {
211. return $this->transactionLevel;
212. }
213. /**
214. * getTransactionLevel
215. * set the current transaction nesting level
216. *
217. * @return Doctrine_Transaction this object
218. */
219. public function setTransactionLevel($level)
220. {
221. $this->transactionLevel = $level;
223. return $this;
224. }
225. /**
226. * beginTransaction
227. * Start a transaction or set a savepoint.
228. *
229. * if trying to set a savepoint and there is no active transaction
230. * a new transaction is being started
231. *
232. * Listeners: onPreTransactionBegin, onTransactionBegin
233. *
234. * @param string $savepoint name of a savepoint to set
235. * @throws Doctrine_Transaction_Exception if the transaction fails at database level
236. * @return integer current transaction nesting level
237. */
238. public function beginTransaction($savepoint = null)
239. {
240. $this->conn->connect();
242. $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
244. if ( ! is_null($savepoint)) {
245. $this->savePoints[] = $savepoint;
247. $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_CREATE);
249. $listener->preSavepointCreate($event);
251. if ( ! $event->skipOperation) {
252. $this->createSavePoint($savepoint);
253. }
255. $listener->postSavepointCreate($event);
256. } else {
257. if ($this->transactionLevel == 0) {
258. $event = new Doctrine_Event($this, Doctrine_Event::TX_BEGIN);
260. $listener->preTransactionBegin($event);
262. if ( ! $event->skipOperation) {
263. try {
264. $this->conn->getDbh()->beginTransaction();
265. } catch(Exception $e) {
266. throw new Doctrine_Transaction_Exception($e->getMessage());
267. }
268. }
269. $listener->postTransactionBegin($event);
270. }
271. }
273. $level = ++$this->transactionLevel;
275. return $level;
276. }
277. /**
278. * commit
279. * Commit the database changes done during a transaction that is in
280. * progress or release a savepoint. This function may only be called when
281. * auto-committing is disabled, otherwise it will fail.
282. *
283. * Listeners: preTransactionCommit, postTransactionCommit
284. *
285. * @param string $savepoint name of a savepoint to release
286. * @throws Doctrine_Transaction_Exception if the transaction fails at database level
287. * @throws Doctrine_Validator_Exception if the transaction fails due to record validations
288. * @return boolean false if commit couldn't be performed, true otherwise
289. */
290. public function commit($savepoint = null)
291. {
292. $this->conn->connect();
294. if ($this->transactionLevel == 0) {
295. return false;
296. }
298. $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
300. if ( ! is_null($savepoint)) {
301. $this->transactionLevel -= $this->removeSavePoints($savepoint);
303. $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_COMMIT);
305. $listener->preSavepointCommit($event);
307. if ( ! $event->skipOperation) {
308. $this->releaseSavePoint($savepoint);
309. }
311. $listener->postSavepointCommit($event);
312. } else {
314. if ($this->transactionLevel == 1) {
315. $event = new Doctrine_Event($this, Doctrine_Event::TX_COMMIT);
317. $listener->preTransactionCommit($event);
319. if ( ! $event->skipOperation) {
320. try {
321. $this->bulkDelete();
323. } catch(Exception $e) {
324. $this->rollback();
326. throw new Doctrine_Transaction_Exception($e->getMessage());
327. }
328. if ( ! empty($this->invalid)) {
329. $this->rollback();
331. $tmp = $this->invalid;
332. $this->invalid = array();
334. throw new Doctrine_Validator_Exception($tmp);
335. }
337. // take snapshots of all collections used within this transaction
338. foreach ($this->_collections as $coll) {
339. $coll->takeSnapshot();
340. }
341. $this->_collections = array();
342. $this->conn->getDbh()->commit();
344. //$this->conn->unitOfWork->reset();
345. }
347. $listener->postTransactionCommit($event);
348. }
350. $this->transactionLevel--;
351. }
353. return true;
354. }
356. /**
357. * rollback
358. * Cancel any database changes done during a transaction or since a specific
359. * savepoint that is in progress. This function may only be called when
360. * auto-committing is disabled, otherwise it will fail. Therefore, a new
361. * transaction is implicitly started after canceling the pending changes.
362. *
363. * this method can be listened with onPreTransactionRollback and onTransactionRollback
364. * eventlistener methods
365. *
366. * @param string $savepoint name of a savepoint to rollback to
367. * @throws Doctrine_Transaction_Exception if the rollback operation fails at database level
368. * @return boolean false if rollback couldn't be performed, true otherwise
369. */
370. public function rollback($savepoint = null)
371. {
372. $this->conn->connect();
374. if ($this->transactionLevel == 0) {
375. return false;
376. }
378. $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
380. if ( ! is_null($savepoint)) {
381. $this->transactionLevel -= $this->removeSavePoints($savepoint);
383. $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_ROLLBACK);
385. $listener->preSavepointRollback($event);
387. if ( ! $event->skipOperation) {
388. $this->rollbackSavePoint($savepoint);
389. }
391. $listener->postSavepointRollback($event);
392. } else {
393. $event = new Doctrine_Event($this, Doctrine_Event::TX_ROLLBACK);
395. $listener->preTransactionRollback($event);
397. if ( ! $event->skipOperation) {
398. $this->deteles = array();
400. $this->transactionLevel = 0;
401. try {
402. $this->conn->getDbh()->rollback();
403. } catch (Exception $e) {
404. throw new Doctrine_Transaction_Exception($e->getMessage());
405. }
406. }
408. $listener->postTransactionRollback($event);
409. }
411. return true;
412. }
414. /**
415. * releaseSavePoint
416. * creates a new savepoint
417. *
418. * @param string $savepoint name of a savepoint to create
419. * @return void
420. */
421. protected function createSavePoint($savepoint)
422. {
423. throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
424. }
426. /**
427. * releaseSavePoint
428. * releases given savepoint
429. *
430. * @param string $savepoint name of a savepoint to release
431. * @return void
432. */
433. protected function releaseSavePoint($savepoint)
434. {
435. throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
436. }
438. /**
439. * rollbackSavePoint
440. * releases given savepoint
441. *
442. * @param string $savepoint name of a savepoint to rollback to
443. * @return void
444. */
445. protected function rollbackSavePoint($savepoint)
446. {
447. throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
448. }
450. /**
451. * removeSavePoints
452. * removes a savepoint from the internal savePoints array of this transaction object
453. * and all its children savepoints
454. *
455. * @param sring $savepoint name of the savepoint to remove
456. * @return integer removed savepoints
457. */
458. private function removeSavePoints($savepoint)
459. {
460. $this->savePoints = array_values($this->savePoints);
462. $found = false;
463. $i = 0;
465. foreach ($this->savePoints as $key => $sp) {
466. if ( ! $found) {
467. if ($sp === $savepoint) {
468. $found = true;
469. }
470. }
471. if ($found) {
472. $i++;
473. unset($this->savePoints[$key]);
474. }
475. }
477. return $i;
478. }
480. /**
481. * setIsolation
482. *
483. * Set the transacton isolation level.
484. * (implemented by the connection drivers)
485. *
486. * example:
487. *
488. * <code>
489. * $tx->setIsolation('READ UNCOMMITTED');
490. * </code>
491. *
492. * @param string standard isolation level
493. * READ UNCOMMITTED (allows dirty reads)
494. * READ COMMITTED (prevents dirty reads)
495. * REPEATABLE READ (prevents nonrepeatable reads)
496. * SERIALIZABLE (prevents phantom reads)
497. *
498. * @throws Doctrine_Transaction_Exception if the feature is not supported by the driver
499. * @throws PDOException if something fails at the PDO level
500. * @return void
501. */
502. public function setIsolation($isolation)
503. {
504. throw new Doctrine_Transaction_Exception('Transaction isolation levels not supported by this driver.');
505. }
507. /**
508. * getTransactionIsolation
509. *
510. * fetches the current session transaction isolation level
511. *
512. * note: some drivers may support setting the transaction isolation level
513. * but not fetching it
514. *
515. * @throws Doctrine_Transaction_Exception if the feature is not supported by the driver
516. * @throws PDOException if something fails at the PDO level
517. * @return string returns the current session transaction isolation level
518. */
519. public function getIsolation()
520. {
521. throw new Doctrine_Transaction_Exception('Fetching transaction isolation level not supported by this driver.');
522. }
523. }