Source for file Transaction.php

Documentation is available at Transaction.php

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