Source for file UnitOfWork.php

Documentation is available at UnitOfWork.php

  1. <?php
  2. /*
  3.  *  $Id: UnitOfWork.php 2197 2007-08-10 20:35:25Z zYne $
  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_Connection_UnitOfWork
  24.  *
  25.  * @package     Doctrine
  26.  * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
  27.  * @category    Object Relational Mapping
  28.  * @link        www.phpdoctrine.com
  29.  * @since       1.0
  30.  * @version     $Revision: 2197 $
  31.  * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
  32.  */
  33. {
  34.     /**
  35.      * buildFlushTree
  36.      * builds a flush tree that is used in transactions
  37.      *
  38.      * The returned array has all the initialized components in
  39.      * 'correct' order. Basically this means that the records of those
  40.      * components can be saved safely in the order specified by the returned array.
  41.      *
  42.      * @param array $tables     an array of Doctrine_Table objects or component names
  43.      * @return array            an array of component names in flushing order
  44.      */
  45.     public function buildFlushTree(array $tables)
  46.     {
  47.         $tree array();
  48.         foreach ($tables as $k => $table{
  49.  
  50.             if ($table instanceof Doctrine_Table)) {
  51.                 $table $this->conn->getTable($tablefalse);
  52.             }
  53.             $nm     $table->getComponentName();
  54.  
  55.             $index  array_search($nm$tree);
  56.  
  57.             if ($index === false{
  58.                 $tree[$nm;
  59.                 $index  max(array_keys($tree));
  60.             }
  61.  
  62.             $rels $table->getRelations();
  63.  
  64.             // group relations
  65.  
  66.             foreach ($rels as $key => $rel{
  67.                 if ($rel instanceof Doctrine_Relation_ForeignKey{
  68.                     unset($rels[$key]);
  69.                     array_unshift($rels$rel);
  70.                 }
  71.             }
  72.  
  73.             foreach ($rels as $rel{
  74.                 $name   $rel->getTable()->getComponentName();
  75.                 $index2 array_search($name,$tree);
  76.                 $type   $rel->getType();
  77.  
  78.                 // skip self-referenced relations
  79.                 if ($name === $nm{
  80.                     continue;
  81.                 }
  82.  
  83.                 if ($rel instanceof Doctrine_Relation_ForeignKey{
  84.                     if ($index2 !== false{
  85.                         if ($index2 >= $index)
  86.                             continue;
  87.  
  88.                         unset($tree[$index]);
  89.                         array_splice($tree,$index2,0,$nm);
  90.                         $index $index2;
  91.                     else {
  92.                         $tree[$name;
  93.                     }
  94.  
  95.                 elseif ($rel instanceof Doctrine_Relation_LocalKey{
  96.                     if ($index2 !== false{
  97.                         if ($index2 <= $index)
  98.                             continue;
  99.  
  100.                         unset($tree[$index2]);
  101.                         array_splice($tree,$index,0,$name);
  102.                     else {
  103.                         array_unshift($tree,$name);
  104.                         $index++;
  105.                     }
  106.                 elseif ($rel instanceof Doctrine_Relation_Association{
  107.                     $t $rel->getAssociationFactory();
  108.                     $n $t->getComponentName();
  109.  
  110.                     if ($index2 !== false)
  111.                         unset($tree[$index2]);
  112.  
  113.                     array_splice($tree$index0$name);
  114.                     $index++;
  115.  
  116.                     $index3 array_search($n$tree);
  117.  
  118.                     if ($index3 !== false{
  119.                         if ($index3 >= $index)
  120.                             continue;
  121.  
  122.                         unset($tree[$index]);
  123.                         array_splice($tree$index30$n);
  124.                         $index $index2;
  125.                     else {
  126.                         $tree[$n;
  127.                     }
  128.                 }
  129.             }
  130.         }
  131.         return array_values($tree);
  132.     }
  133.     /**
  134.      * saves the given record
  135.      *
  136.      * @param Doctrine_Record $record 
  137.      * @return void 
  138.      */
  139.     public function saveGraph(Doctrine_Record $record)
  140.     {
  141.         $conn $this->getConnection();
  142.  
  143.         $state $record->state();
  144.         if ($state === Doctrine_Record::STATE_LOCKED{
  145.             return false;
  146.         }
  147.  
  148.         $record->state(Doctrine_Record::STATE_LOCKED);
  149.  
  150.         $conn->beginTransaction();
  151.  
  152.         $saveLater $this->saveRelated($record);
  153.  
  154.         $record->state($state);
  155.  
  156.         if ($record->isValid()) {
  157.             $event new Doctrine_Event($recordDoctrine_Event::RECORD_SAVE);
  158.  
  159.             $record->preSave($event);
  160.     
  161.             $record->getTable()->getRecordListener()->preSave($event);
  162.  
  163.             if $event->skipOperation{
  164.                 switch ($state{
  165.                     case Doctrine_Record::STATE_TDIRTY:
  166.                         $this->insert($record);
  167.                         break;
  168.                     case Doctrine_Record::STATE_DIRTY:
  169.                     case Doctrine_Record::STATE_PROXY:
  170.                         $this->update($record);
  171.                         break;
  172.                     case Doctrine_Record::STATE_CLEAN:
  173.                     case Doctrine_Record::STATE_TCLEAN:
  174.  
  175.                         break;
  176.                 }
  177.             }
  178.  
  179.             $record->getTable()->getRecordListener()->postSave($event);
  180.             
  181.             $record->postSave($event);
  182.         else {
  183.             $conn->transaction->addInvalid($record);
  184.         }
  185.         
  186.         $state $record->state();
  187.  
  188.         $record->state(Doctrine_Record::STATE_LOCKED);
  189.  
  190.         foreach ($saveLater as $fk{
  191.             $alias $fk->getAlias();
  192.  
  193.             if ($record->hasReference($alias)) {
  194.                 $obj $record->$alias;
  195.                 $obj->save($conn);
  196.             }
  197.         }
  198.  
  199.         // save the MANY-TO-MANY associations
  200.         $this->saveAssociations($record);
  201.  
  202.         $record->state($state);
  203.  
  204.         $conn->commit();
  205.  
  206.         return true;
  207.     }
  208.     /**
  209.      * saves the given record
  210.      *
  211.      * @param Doctrine_Record $record 
  212.      * @return void 
  213.      */
  214.     public function save(Doctrine_Record $record)
  215.     {
  216.         $event new Doctrine_Event($recordDoctrine_Event::RECORD_SAVE);
  217.  
  218.         $record->preSave($event);
  219.  
  220.         $record->getTable()->getRecordListener()->preSave($event);
  221.  
  222.         if $event->skipOperation{
  223.             switch ($record->state()) {
  224.                 case Doctrine_Record::STATE_TDIRTY:
  225.                     $this->insert($record);
  226.                     break;
  227.                 case Doctrine_Record::STATE_DIRTY:
  228.                 case Doctrine_Record::STATE_PROXY:
  229.                     $this->update($record);
  230.                     break;
  231.                 case Doctrine_Record::STATE_CLEAN:
  232.                 case Doctrine_Record::STATE_TCLEAN:
  233.                     // do nothing
  234.                     break;
  235.             }
  236.         }
  237.  
  238.         $record->getTable()->getRecordListener()->postSave($event);
  239.         
  240.         $record->postSave($event);
  241.     }
  242.     /**
  243.      * deletes given record and all the related composites
  244.      * this operation is isolated by a transaction
  245.      *
  246.      * this event can be listened by the onPreDelete and onDelete listeners
  247.      *
  248.      * @return boolean      true on success, false on failure
  249.      */
  250.     public function delete(Doctrine_Record $record)
  251.     {
  252.         if $record->exists()) {
  253.             return false;
  254.         }
  255.         $this->conn->beginTransaction();
  256.  
  257.         $event new Doctrine_Event($recordDoctrine_Event::RECORD_DELETE);
  258.  
  259.         $record->preDelete($event);
  260.         
  261.         $record->getTable()->getRecordListener()->preDelete($event);
  262.  
  263.         $record->state(Doctrine_Record::STATE_LOCKED);
  264.  
  265.         $this->deleteComposites($record);
  266.         
  267.         $record->state(Doctrine_Record::STATE_TDIRTY);
  268.  
  269.         if $event->skipOperation{
  270.             $this->conn->transaction->addDelete($record);
  271.  
  272.             $record->state(Doctrine_Record::STATE_TCLEAN);
  273.         }
  274.         
  275.         $record->getTable()->getRecordListener()->postDelete($event);
  276.  
  277.         $record->postDelete($event);
  278.  
  279.         $this->conn->commit();
  280.  
  281.         return true;
  282.     }
  283.  
  284.     /**
  285.      * saveRelated
  286.      * saves all related records to $record
  287.      *
  288.      * @throws PDOException         if something went wrong at database level
  289.      * @param Doctrine_Record $record 
  290.      */
  291.     public function saveRelated(Doctrine_Record $record)
  292.     {
  293.         $saveLater array();
  294.         foreach ($record->getReferences(as $k => $v{
  295.             $rel $record->getTable()->getRelation($k);
  296.  
  297.             $local $rel->getLocal();
  298.             $foreign $rel->getForeign();
  299.  
  300.             if ($rel instanceof Doctrine_Relation_ForeignKey{
  301.                 $saveLater[$k$rel;
  302.             elseif ($rel instanceof Doctrine_Relation_LocalKey{
  303.                 // ONE-TO-ONE relationship
  304.                 $obj $record->get($rel->getAlias());
  305.  
  306.                 // Protection against infinite function recursion before attempting to save
  307.                 if ($obj instanceof Doctrine_Record &&
  308.                     $obj->isModified()) {
  309.                     $obj->save($this->conn);
  310.                 }
  311.             }
  312.         }
  313.  
  314.         return $saveLater;
  315.     }
  316.     /**
  317.      * saveAssociations
  318.      *
  319.      * this method takes a diff of one-to-many / many-to-many original and
  320.      * current collections and applies the changes
  321.      *
  322.      * for example if original many-to-many related collection has records with
  323.      * primary keys 1,2 and 3 and the new collection has records with primary keys
  324.      * 3, 4 and 5, this method would first destroy the associations to 1 and 2 and then
  325.      * save new associations to 4 and 5
  326.      *
  327.      * @throws PDOException         if something went wrong at database level
  328.      * @param Doctrine_Record $record 
  329.      * @return void 
  330.      */
  331.     public function saveAssociations(Doctrine_Record $record)
  332.     {
  333.         foreach ($record->getReferences(as $k => $v{
  334.             $rel $record->getTable()->getRelation($k);
  335.             
  336.             if ($rel instanceof Doctrine_Relation_Association{   
  337.                 $v->save($this->conn);
  338.  
  339.                 $assocTable $rel->getAssociationTable();
  340.                 foreach ($v->getDeleteDiff(as $r{
  341.                     $query 'DELETE FROM ' $assocTable->getTableName()
  342.                            . ' WHERE ' $rel->getForeign(' = ?'
  343.                            . ' AND ' $rel->getLocal(' = ?';
  344.  
  345.                     $this->conn->execute($queryarray($r->getIncremented()$record->getIncremented()));
  346.                 }
  347.  
  348.                 foreach ($v->getInsertDiff(as $r{
  349.                     $assocRecord $assocTable->create();
  350.                     $assocRecord->set($rel->getForeign()$r);
  351.                     $assocRecord->set($rel->getLocal()$record);
  352.  
  353.                     $this->saveGraph($assocRecord);
  354.                 }
  355.             }
  356.         }
  357.     }
  358.     /**
  359.      * deletes all related composites
  360.      * this method is always called internally when a record is deleted
  361.      *
  362.      * @throws PDOException         if something went wrong at database level
  363.      * @return void 
  364.      */
  365.     public function deleteComposites(Doctrine_Record $record)
  366.     {
  367.         foreach ($record->getTable()->getRelations(as $fk{
  368.             switch ($fk->getType()) {
  369.                 case Doctrine_Relation::ONE_COMPOSITE:
  370.                 case Doctrine_Relation::MANY_COMPOSITE:
  371.                     $obj $record->get($fk->getAlias());
  372.                     if $obj instanceof Doctrine_Record && 
  373.                            $obj->state(!= Doctrine_Record::STATE_LOCKED)  {
  374.                             
  375.                             $obj->delete($this->conn);
  376.                                
  377.                     }
  378.                     break;
  379.             }
  380.         }
  381.     }
  382.     /**
  383.      * saveAll
  384.      * persists all the pending records from all tables
  385.      *
  386.      * @throws PDOException         if something went wrong at database level
  387.      * @return void 
  388.      */
  389.     public function saveAll()
  390.     {
  391.         // get the flush tree
  392.         $tree $this->buildFlushTree($this->conn->getTables());
  393.  
  394.         // save all records
  395.         foreach ($tree as $name{
  396.             $table $this->conn->getTable($name);
  397.  
  398.             foreach ($table->getRepository(as $record{
  399.                 $this->save($record);
  400.             }
  401.         }
  402.  
  403.         // save all associations
  404.         foreach ($tree as $name{
  405.             $table $this->conn->getTable($name);
  406.  
  407.             foreach ($table->getRepository(as $record{
  408.                 $this->saveAssociations($record);
  409.             }
  410.         }
  411.     }
  412.     /**
  413.      * update
  414.      * updates the given record
  415.      *
  416.      * @param Doctrine_Record $record   record to be updated
  417.      * @return boolean                  whether or not the update was successful
  418.      */
  419.     public function update(Doctrine_Record $record)
  420.     {
  421.         $event new Doctrine_Event($recordDoctrine_Event::RECORD_UPDATE);
  422.  
  423.         $record->preUpdate($event);
  424.  
  425.         $record->getTable()->getRecordListener()->preUpdate($event);
  426.  
  427.         if $event->skipOperation{
  428.             $array $record->getPrepared();
  429.  
  430.             if (empty($array)) {
  431.                 return false;
  432.             }
  433.             $set array();
  434.             foreach ($array as $name => $value{
  435.                 if ($value instanceof Doctrine_Expression{
  436.                     $set[$value->getSql();
  437.                     unset($array[$name]);
  438.                 else {
  439.  
  440.                     $set[$name ' = ?';
  441.     
  442.                     if ($value instanceof Doctrine_Record{
  443.                         if $value->exists()) {
  444.                             $record->save($this->conn);
  445.                         }
  446.                         $array[$name$value->getIncremented();
  447.                         $record->set($name$value->getIncremented());
  448.                     }
  449.                 }
  450.             }
  451.  
  452.             $params array_values($array);
  453.             $id     $record->identifier();
  454.     
  455.             if is_array($id)) {
  456.                 $id array($id);
  457.             }
  458.             $id     array_values($id);
  459.             $params array_merge($params$id);
  460.     
  461.             $sql  'UPDATE ' $this->conn->quoteIdentifier($record->getTable()->getTableName())
  462.                   . ' SET ' implode(', '$set)
  463.                   . ' WHERE ' implode(' = ? AND '$record->getTable()->getPrimaryKeys())
  464.                   . ' = ?';
  465.     
  466.             $stmt $this->conn->prepare($sql);
  467.             $stmt->execute($params);
  468.     
  469.             $record->assignIdentifier(true);
  470.         }
  471.         
  472.         $record->getTable()->getRecordListener()->postUpdate($event);
  473.  
  474.         $record->postUpdate($event);
  475.  
  476.         return true;
  477.     }
  478.     /**
  479.      * inserts a record into database
  480.      *
  481.      * @param Doctrine_Record $record   record to be inserted
  482.      * @return boolean 
  483.      */
  484.     public function insert(Doctrine_Record $record)
  485.     {
  486.          // listen the onPreInsert event
  487.         $event new Doctrine_Event($recordDoctrine_Event::RECORD_INSERT);
  488.  
  489.         $record->preInsert($event);
  490.         
  491.         $record->getTable()->getRecordListener()->preInsert($event);
  492.  
  493.         if $event->skipOperation{
  494.             $array $record->getPrepared();
  495.     
  496.             if (empty($array)) {
  497.                 return false;
  498.             }
  499.             $table     $record->getTable();
  500.             $keys      $table->getPrimaryKeys();
  501.     
  502.             $seq       $record->getTable()->sequenceName;
  503.     
  504.             if empty($seq)) {
  505.                 $id             $this->conn->sequence->nextId($seq);
  506.                 $name           $record->getTable()->getIdentifier();
  507.                 $array[$name]   $id;
  508.     
  509.                 $record->assignIdentifier($id);
  510.             }
  511.     
  512.             $this->conn->insert($table->getTableName()$array);
  513.     
  514.             if (empty($seq&& count($keys== && $keys[0== $table->getIdentifier(&&
  515.                 $table->getIdentifierType(!= Doctrine::IDENTIFIER_NATURAL{
  516.     
  517.                 if (strtolower($this->conn->getName()) == 'pgsql'{
  518.                     $seq $table->getTableName('_' $keys[0];
  519.                 }
  520.     
  521.                 $id $this->conn->sequence->lastInsertId($seq);
  522.     
  523.                 if $id{
  524.                     $id $table->getMaxIdentifier();
  525.                 }
  526.     
  527.                 $record->assignIdentifier($id);
  528.             else {
  529.                 $record->assignIdentifier(true);
  530.             }
  531.         }
  532.         $record->getTable()->addRecord($record);
  533.  
  534.         $record->getTable()->getRecordListener()->postInsert($event);
  535.  
  536.         $record->postInsert($event);
  537.  
  538.         return true;
  539.     }
  540. }