Coverage for Doctrine_Transaction

Back to coverage report

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