Coverage for Doctrine_Transaction

Back to coverage report

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