Coverage for Doctrine_Transaction

Back to coverage report

1 <?php
2 /*
3  *  $Id: Transaction.php 2963 2007-10-21 06:23:59Z Jonathan.Wage $
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: 2963 $
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      * getTransactionLevel
130      * get the current transaction nesting level
131      *
132      * @return integer
133      */
134     public function getTransactionLevel()
135     {
136         return $this->transactionLevel;
137     }
138
139     /**
140      * getTransactionLevel
141      * set the current transaction nesting level
142      *
143      * @return Doctrine_Transaction     this object
144      */
145     public function setTransactionLevel($level)
146     {
147         $this->transactionLevel = $level;
148
149         return $this;
150     }
151
152     /**
153      * beginTransaction
154      * Start a transaction or set a savepoint.
155      *
156      * if trying to set a savepoint and there is no active transaction
157      * a new transaction is being started
158      *
159      * Listeners: onPreTransactionBegin, onTransactionBegin
160      *
161      * @param string $savepoint                 name of a savepoint to set
162      * @throws Doctrine_Transaction_Exception   if the transaction fails at database level     
163      * @return integer                          current transaction nesting level
164      */
165     public function beginTransaction($savepoint = null)
166     {
167         $this->conn->connect();
168         
169         $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
170
171         if ( ! is_null($savepoint)) {
172             $this->savePoints[] = $savepoint;
173
174             $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_CREATE);
175
176             $listener->preSavepointCreate($event);
177
178             if ( ! $event->skipOperation) {
179                 $this->createSavePoint($savepoint);
180             }
181
182             $listener->postSavepointCreate($event);
183         } else {
184             if ($this->transactionLevel == 0) {
185                 $event = new Doctrine_Event($this, Doctrine_Event::TX_BEGIN);
186
187                 $listener->preTransactionBegin($event);
188
189                 if ( ! $event->skipOperation) {
190                     try {
191                         $this->conn->getDbh()->beginTransaction();
192                     } catch(Exception $e) {
193                         throw new Doctrine_Transaction_Exception($e->getMessage());
194                     }
195                 }
196                 $listener->postTransactionBegin($event);
197             }
198         }
199
200         $level = ++$this->transactionLevel;
201
202         return $level;
203     }
204
205     /**
206      * commit
207      * Commit the database changes done during a transaction that is in
208      * progress or release a savepoint. This function may only be called when
209      * auto-committing is disabled, otherwise it will fail.
210      *
211      * Listeners: preTransactionCommit, postTransactionCommit
212      *
213      * @param string $savepoint                 name of a savepoint to release
214      * @throws Doctrine_Transaction_Exception   if the transaction fails at database level
215      * @throws Doctrine_Validator_Exception     if the transaction fails due to record validations
216      * @return boolean                          false if commit couldn't be performed, true otherwise
217      */
218     public function commit($savepoint = null)
219     {
220         $this->conn->connect();
221
222         if ($this->transactionLevel == 0) {
223             return false;
224         }
225
226         $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
227
228         if ( ! is_null($savepoint)) {
229             $this->transactionLevel -= $this->removeSavePoints($savepoint);
230
231             $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_COMMIT);
232
233             $listener->preSavepointCommit($event);
234
235             if ( ! $event->skipOperation) {
236                 $this->releaseSavePoint($savepoint);
237             }
238
239             $listener->postSavepointCommit($event);
240         } else {
241
242             if ($this->transactionLevel == 1) {  
243                 if ( ! empty($this->invalid)) {
244                     $this->rollback();
245
246                     $tmp = $this->invalid;
247                     $this->invalid = array();
248
249                     throw new Doctrine_Validator_Exception($tmp);
250                 }
251
252                 // take snapshots of all collections used within this transaction
253                 foreach ($this->_collections as $coll) {
254                     $coll->takeSnapshot();
255                 }
256                 $this->_collections = array();
257                 
258                 $event = new Doctrine_Event($this, Doctrine_Event::TX_COMMIT);
259             
260                 $listener->preTransactionCommit($event);
261                 if ( ! $event->skipOperation) {
262                     $this->conn->getDbh()->commit();
263                 }
264                 $listener->postTransactionCommit($event);
265
266             }
267             
268             $this->transactionLevel--;
269         }
270
271         return true;
272     }
273
274     /**
275      * rollback
276      * Cancel any database changes done during a transaction or since a specific
277      * savepoint that is in progress. This function may only be called when
278      * auto-committing is disabled, otherwise it will fail. Therefore, a new
279      * transaction is implicitly started after canceling the pending changes.
280      *
281      * this method can be listened with onPreTransactionRollback and onTransactionRollback
282      * eventlistener methods
283      *
284      * @param string $savepoint                 name of a savepoint to rollback to   
285      * @throws Doctrine_Transaction_Exception   if the rollback operation fails at database level
286      * @return boolean                          false if rollback couldn't be performed, true otherwise
287      */
288     public function rollback($savepoint = null)
289     {
290         $this->conn->connect();
291
292         if ($this->transactionLevel == 0) {
293             return false;
294         }
295
296         $listener = $this->conn->getAttribute(Doctrine::ATTR_LISTENER);
297
298         if ( ! is_null($savepoint)) {
299             $this->transactionLevel -= $this->removeSavePoints($savepoint);
300
301             $event = new Doctrine_Event($this, Doctrine_Event::SAVEPOINT_ROLLBACK);
302
303             $listener->preSavepointRollback($event);
304             
305             if ( ! $event->skipOperation) {
306                 $this->rollbackSavePoint($savepoint);
307             }
308
309             $listener->postSavepointRollback($event);
310         } else {
311             $event = new Doctrine_Event($this, Doctrine_Event::TX_ROLLBACK);
312     
313             $listener->preTransactionRollback($event);
314             
315             if ( ! $event->skipOperation) {
316                 $this->transactionLevel = 0;
317                 try {
318                     $this->conn->getDbh()->rollback();
319                 } catch (Exception $e) {
320                     throw new Doctrine_Transaction_Exception($e->getMessage());
321                 }
322             }
323
324             $listener->postTransactionRollback($event);
325         }
326
327         return true;
328     }
329
330     /**
331      * releaseSavePoint
332      * creates a new savepoint
333      *
334      * @param string $savepoint     name of a savepoint to create
335      * @return void
336      */
337     protected function createSavePoint($savepoint)
338     {
339         throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
340     }
341
342     /**
343      * releaseSavePoint
344      * releases given savepoint
345      *
346      * @param string $savepoint     name of a savepoint to release
347      * @return void
348      */
349     protected function releaseSavePoint($savepoint)
350     {
351         throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
352     }
353
354     /**
355      * rollbackSavePoint
356      * releases given savepoint
357      *
358      * @param string $savepoint     name of a savepoint to rollback to
359      * @return void
360      */
361     protected function rollbackSavePoint($savepoint)
362     {
363         throw new Doctrine_Transaction_Exception('Savepoints not supported by this driver.');
364     }
365
366     /**
367      * removeSavePoints
368      * removes a savepoint from the internal savePoints array of this transaction object
369      * and all its children savepoints
370      *
371      * @param sring $savepoint      name of the savepoint to remove
372      * @return integer              removed savepoints
373      */
374     private function removeSavePoints($savepoint)
375     {
376         $this->savePoints = array_values($this->savePoints);
377
378         $found = false;
379         $i = 0;
380
381         foreach ($this->savePoints as $key => $sp) {
382             if ( ! $found) {
383                 if ($sp === $savepoint) {
384                     $found = true;
385                 }
386             }
387             if ($found) {
388                 $i++;
389                 unset($this->savePoints[$key]);
390             }
391         }
392
393         return $i;
394     }
395
396     /**
397      * setIsolation
398      *
399      * Set the transacton isolation level.
400      * (implemented by the connection drivers)
401      *
402      * example:
403      *
404      * <code>
405      * $tx->setIsolation('READ UNCOMMITTED');
406      * </code>
407      *
408      * @param   string  standard isolation level
409      *                  READ UNCOMMITTED (allows dirty reads)
410      *                  READ COMMITTED (prevents dirty reads)
411      *                  REPEATABLE READ (prevents nonrepeatable reads)
412      *                  SERIALIZABLE (prevents phantom reads)
413      *
414      * @throws Doctrine_Transaction_Exception           if the feature is not supported by the driver
415      * @throws PDOException                             if something fails at the PDO level
416      * @return void
417      */
418     public function setIsolation($isolation)
419     {
420         throw new Doctrine_Transaction_Exception('Transaction isolation levels not supported by this driver.');
421     }
422
423     /**
424      * getTransactionIsolation
425      *
426      * fetches the current session transaction isolation level
427      *
428      * note: some drivers may support setting the transaction isolation level
429      * but not fetching it
430      *
431      * @throws Doctrine_Transaction_Exception           if the feature is not supported by the driver
432      * @throws PDOException                             if something fails at the PDO level
433      * @return string                                   returns the current session transaction isolation level
434      */
435     public function getIsolation()
436     {
437         throw new Doctrine_Transaction_Exception('Fetching transaction isolation level not supported by this driver.');
438     }
439 }