Coverage for Doctrine_Transaction

Back to coverage report

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