# Coverage for Doctrine_Transaction

Back to coverage report

1 | <?php
2 | /*
3 | * $Id: Transaction.php 3184 2007-11-18 16:42:33Z 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_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 | * @subpackage Transaction
31 | * @link www.phpdoctrine.org
32 | * @since 1.0
33 | * @version $Revision: 3184 $
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 | /**
43 | * Doctrine_Transaction is in active state when it has one active transaction
44 | */
45 | const STATE_ACTIVE = 1;
46 |
47 | /**
48 | * Doctrine_Transaction is in busy state when it has multiple active transactions
49 | */
50 | const STATE_BUSY = 2;
51 |
52 | /**
53 | * @var integer $transactionLevel the nesting level of transactions, used by transaction methods
54 | */
55 | protected $transactionLevel = 0;
56 |
57 | /**
58 | * @var array $invalid an array containing all invalid records within this transaction
59 | * @todo What about a more verbose name? $invalidRecords?
60 | */
61 | protected $invalid = array();
62 |
63 | /**
64 | * @var array $savepoints an array containing all savepoints
65 | */
66 | protected $savePoints = array();
67 |
68 | /**
69 | * @var array $_collections an array of Doctrine_Collection objects that were affected during the Transaction
70 | */
71 | protected $_collections = array();
72 |
73 | /**
74 | * addCollection
75 | * adds a collection in the internal array of collections
76 | *
77 | * at the end of each commit this array is looped over and
78 | * of every collection Doctrine then takes a snapshot in order
79 | * to keep the collections up to date with the database
80 | *
81 | * @param Doctrine_Collection $coll a collection to be added
82 | * @return Doctrine_Transaction this object
83 | */
84 | public function addCollection(Doctrine_Collection $coll)
85 | {
86 | $this->_collections[] = $coll;
87 |
88 | return $this;
89 | }
90 |
91 | /**
92 | * getState
93 | * returns the state of this connection
94 | *
95 | * @see Doctrine_Connection_Transaction::STATE_* constants
96 | * @return integer the connection state
97 | */
98 | public function getState()
99 | {
100 | switch ($this->transactionLevel) {
101 | case 0:
102 | return Doctrine_Transaction::STATE_SLEEP;
103 | break;
104 | case 1:
105 | return Doctrine_Transaction::STATE_ACTIVE;
106 | break;
107 | default:
108 | return Doctrine_Transaction::STATE_BUSY;
109 | }
110 | }
111 |
112 | /**
113 | * addInvalid
114 | * adds record into invalid records list
115 | *
116 | * @param Doctrine_Record $record
117 | * @return boolean false if record already existed in invalid records list,
118 | * otherwise true
119 | */
120 | public function addInvalid(Doctrine_Record $record)
121 | {
122 | if (in_array($record, $this->invalid, true)) {
123 | return false;
124 | }
125 | $this->invalid[] = $record;
126 | return true;
127 | }
128 |
129 |
130 | /**
131 | * Return the invalid records
132 | *
133 | * @return array An array of invalid records
134 | */
135 | public function getInvalid(){
136 | return $this->invalid;
137 | }
138 |
139 | /**
140 | * getTransactionLevel
141 | * get the current transaction nesting level
142 | *
143 | * @return integer
144 | */
145 | public function getTransactionLevel()
146 | {
147 | return $this->transactionLevel;
148 | }
149 |
150 | /**
151 | * getTransactionLevel
152 | * set the current transaction nesting level
153 | *
154 | * @return Doctrine_Transaction this object
155 | */
156 | public function setTransactionLevel($level)
157 | {
158 | $this->transactionLevel = $level;
159 |
160 | return $this;
161 | }
162 |
163 | /**
164 | * beginTransaction
165 | * Start a transaction or set a savepoint.
166 | *
167 | * if trying to set a savepoint and there is no active transaction
168 | * a new transaction is being started
169 | *
170 | * Listeners: onPreTransactionBegin, onTransactionBegin
171 | *
172 | * @param string $savepoint name of a savepoint to set
173 | * @throws Doctrine_Transaction_Exception if the transaction fails at database level
174 | * @return integer current transaction nesting level
175 | */
176 | public function beginTransaction($savepoint = null)
177 | {
178 | $this->conn->connect();
179 |
180 | $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
181 |
182 | if ( ! is_null($savepoint)) {
183 | $this->savePoints[] = $savepoint;
184 |
185 | $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_CREATE);
186 |
187 | $listener->preSavepointCreate($event);
188 |
189 | if ( ! $event->skipOperation) {
190 | $this->createSavePoint($savepoint);
191 | }
192 |
193 | $listener->postSavepointCreate($event);
194 | } else {
195 | if ($this->transactionLevel == 0) {
196 | $event = new Doctrine_Event($this, Doctrine_Event::TX_BEGIN);
197 |
198 | $listener->preTransactionBegin($event);
199 |
200 | if ( ! $event->skipOperation) {
201 | try {
202 | $this->conn->getDbh()->beginTransaction();
203 | } catch(Exception $e) {
204 | throw new Doctrine_Transaction_Exception($e->getMessage());
205 | }
206 | }
207 | $listener->postTransactionBegin($event);
208 | }
209 | }
210 |
211 | $level = ++$this->transactionLevel;
212 |
213 | return $level;
214 | }
215 |
216 | /**
217 | * commit
218 | * Commit the database changes done during a transaction that is in
219 | * progress or release a savepoint. This function may only be called when
220 | * auto-committing is disabled, otherwise it will fail.
221 | *
222 | * Listeners: preTransactionCommit, postTransactionCommit
223 | *
224 | * @param string $savepoint name of a savepoint to release
225 | * @throws Doctrine_Transaction_Exception if the transaction fails at database level
226 | * @throws Doctrine_Validator_Exception if the transaction fails due to record validations
227 | * @return boolean false if commit couldn't be performed, true otherwise
228 | */
229 | public function commit($savepoint = null)
230 | {
231 | $this->conn->connect();
232 |
233 | if ($this->transactionLevel == 0) {
234 | return false;
235 | }
236 |
237 | $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
238 |
239 | if ( ! is_null($savepoint)) {
240 | $this->transactionLevel -= $this->removeSavePoints($savepoint);
241 |
242 | $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_COMMIT);
243 |
244 | $listener->preSavepointCommit($event);
245 |
246 | if ( ! $event->skipOperation) {
247 | $this->releaseSavePoint($savepoint);
248 | }
249 |
250 | $listener->postSavepointCommit($event);
251 | } else {
252 | if ($this->transactionLevel == 1) {
253 | if ( ! empty($this->invalid)) {
254 | $this->rollback();
255 | $tmp = $this->invalid;
256 | $this->invalid = array();
257 | throw new Doctrine_Validator_Exception($tmp);
258 | }
259 | // take snapshots of all collections used within this transaction
260 | foreach ($this->_collections as $coll) {
261 | $coll->takeSnapshot();
262 | }
263 | $this->_collections = array();
264 |
265 | $event = new Doctrine_Event($this, Doctrine_Event::TX_COMMIT);
266 |
267 | $listener->preTransactionCommit($event);
268 | if ( ! $event->skipOperation) {
269 | $this->conn->getDbh()->commit();
270 | }
271 | $listener->postTransactionCommit($event);
272 |
273 | }
274 |
275 | $this->transactionLevel--;
276 | }
277 |
278 | return true;
279 | }
280 |
281 | /**
282 | * rollback
283 | * Cancel any database changes done during a transaction or since a specific
284 | * savepoint that is in progress. This function may only be called when
285 | * auto-committing is disabled, otherwise it will fail. Therefore, a new
286 | * transaction is implicitly started after canceling the pending changes.
287 | *
288 | * this method can be listened with onPreTransactionRollback and onTransactionRollback
289 | * eventlistener methods
290 | *
291 | * @param string $savepoint name of a savepoint to rollback to
292 | * @throws Doctrine_Transaction_Exception if the rollback operation fails at database level
293 | * @return boolean false if rollback couldn't be performed, true otherwise
294 | * @todo Shouldnt this method only commit a rollback if the transactionLevel is 1
295 | * (STATE_ACTIVE)? Explanation: Otherwise a rollback that is triggered from inside doctrine
296 | * in an (emulated) nested transaction would lead to a complete database level
297 | * rollback even though the client code did not yet want to do that.
298 | * In other words: if the user starts a transaction doctrine shouldnt roll it back.
299 | * Doctrine should only roll back transactions started by doctrine. Thoughts?
300 | */
301 | public function rollback($savepoint = null)
302 | {
303 | $this->conn->connect();
304 | $currentState = $this->getState();
305 |
306 | if ($currentState != self::STATE_ACTIVE && $currentState != self::STATE_BUSY) {
307 | return false;
308 | }
309 |
310 | $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
311 |
312 | if ( ! is_null($savepoint)) {
313 | $this->transactionLevel -= $this->removeSavePoints($savepoint);
314 |
315 | $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_ROLLBACK);
316 |
317 | $listener->preSavepointRollback($event);
318 |
319 | if ( ! $event->skipOperation) {
320 | $this->rollbackSavePoint($savepoint);
321 | }
322 |
323 | $listener->postSavepointRollback($event);
324 | } else {
325 | $event = new Doctrine_Event($this, Doctrine_Event::TX_ROLLBACK);
326 |
327 | $listener->preTransactionRollback($event);
328 |
329 | if ( ! $event->skipOperation) {
330 | $this->transactionLevel = 0;
331 | try {
332 | $this->conn->getDbh()->rollback();
333 | } catch (Exception $e) {
334 | throw new Doctrine_Transaction_Exception($e->getMessage());
335 | }
336 | }
337 |
338 | $listener->postTransactionRollback($event);
339 | }
340 |
341 | return true;
342 | }
343 |
344 | /**
345 | * releaseSavePoint
346 | * creates a new savepoint
347 | *
348 | * @param string $savepoint name of a savepoint to create
349 | * @return void
350 | */
351 | protected function createSavePoint($savepoint)
352 | {
353 | throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
354 | }
355 |
356 | /**
357 | * releaseSavePoint
358 | * releases given savepoint
359 | *
360 | * @param string $savepoint name of a savepoint to release
361 | * @return void
362 | */
363 | protected function releaseSavePoint($savepoint)
364 | {
365 | throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
366 | }
367 |
368 | /**
369 | * rollbackSavePoint
370 | * releases given savepoint
371 | *
372 | * @param string $savepoint name of a savepoint to rollback to
373 | * @return void
374 | */
375 | protected function rollbackSavePoint($savepoint)
376 | {
377 | throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
378 | }
379 |
380 | /**
381 | * removeSavePoints
382 | * removes a savepoint from the internal savePoints array of this transaction object
383 | * and all its children savepoints
384 | *
385 | * @param sring $savepoint name of the savepoint to remove
386 | * @return integer removed savepoints
387 | */
388 | private function removeSavePoints($savepoint)
389 | {
390 | $this->savePoints = array_values($this->savePoints);
391 |
392 | $found = false;
393 | $i = 0;
394 |
395 | foreach ($this->savePoints as $key => $sp) {
396 | if ( ! $found) {
397 | if ($sp === $savepoint) {
398 | $found = true;
399 | }
400 | }
401 | if ($found) {
402 | $i++;
403 | unset($this->savePoints[$key]);
404 | }
405 | }
406 |
407 | return $i;
408 | }
409 |
410 | /**
411 | * setIsolation
412 | *
413 | * Set the transacton isolation level.
414 | * (implemented by the connection drivers)
415 | *
416 | * example:
417 | *
418 | * <code>
419 | * $tx->setIsolation('READ UNCOMMITTED');
420 | * </code>
421 | *
422 | * @param string standard isolation level
423 | * READ UNCOMMITTED (allows dirty reads)
424 | * READ COMMITTED (prevents dirty reads)
425 | * REPEATABLE READ (prevents nonrepeatable reads)
426 | * SERIALIZABLE (prevents phantom reads)
427 | *
428 | * @throws Doctrine_Transaction_Exception if the feature is not supported by the driver
429 | * @throws PDOException if something fails at the PDO level
430 | * @return void
431 | */
432 | public function setIsolation($isolation)
433 | {
434 | throw new Doctrine_Transaction_Exception('Transaction isolation levels not supported by this driver.');
435 | }
436 |
437 | /**
438 | * getTransactionIsolation
439 | *
440 | * fetches the current session transaction isolation level
441 | *
442 | * note: some drivers may support setting the transaction isolation level
443 | * but not fetching it
444 | *
445 | * @throws Doctrine_Transaction_Exception if the feature is not supported by the driver
446 | * @throws PDOException if something fails at the PDO level
447 | * @return string returns the current session transaction isolation level
448 | */
449 | public function getIsolation()
450 | {
451 | throw new Doctrine_Transaction_Exception('Fetching transaction isolation level not supported by this driver.');
452 | }
453 | }