Author: romanb
Date: 2008-09-12 10:44:51 +0100 (Fri, 12 Sep 2008)
New Revision: 4921

Added:
   trunk/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php
   trunk/lib/Doctrine/ORM/Persisters/Exception.php
   trunk/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
   trunk/lib/Doctrine/ORM/Persisters/SingleTablePersister.php
   trunk/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php
   trunk/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php
Removed:
   trunk/lib/Doctrine/EntityPersister/
Modified:
   trunk/tests/Orm/EntityPersisterTest.php
   trunk/tests/lib/mocks/Doctrine_EntityPersisterMock.php
Log:
moved entitypersisters

Added: trunk/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php               
                (rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/AbstractEntityPersister.php       
2008-09-12 09:44:51 UTC (rev 4921)
@@ -0,0 +1,654 @@
+<?php 
+/*
+ *  $Id$
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the LGPL. For more information, see
+ * <http://www.phpdoctrine.org>.
+ */
+
+#namespace Doctrine::ORM::Persisters;
+
+/**
+ * Base class for all EntityPersisters.
+ *
+ * @author      Roman Borschel <[EMAIL PROTECTED]>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @version     $Revision: 3406 $
+ * @link        www.phpdoctrine.org
+ * @since       2.0
+ * @todo Rename to AbstractEntityPersister
+ */
+abstract class Doctrine_ORM_Persisters_AbstractEntityPersister
+{
+    /**
+     * The names of all the fields that are available on entities. 
+     */
+    protected $_fieldNames = array();
+    
+    /**
+     * Metadata object that descibes the mapping of the mapped entity class.
+     *
+     * @var Doctrine_ClassMetadata
+     */
+    protected $_classMetadata;
+    
+    /**
+     * The name of the Entity the persister is used for.
+     * 
+     * @var string
+     */
+    protected $_entityName;
+
+    /**
+     * The Doctrine_Connection object that the database connection of this 
mapper.
+     *
+     * @var Doctrine::DBAL::Connection $conn
+     */
+    protected $_conn;
+    
+    /**
+     * The EntityManager.
+     *
+     * @var Doctrine::ORM::EntityManager
+     */
+    protected $_em;
+    
+    /**
+     * Null object.
+     */
+    private $_nullObject;
+
+    /**
+     * Constructs a new EntityPersister.
+     */
+    public function __construct(Doctrine_EntityManager $em, 
Doctrine_ClassMetadata $classMetadata)
+    {
+        $this->_em = $em;
+        $this->_entityName = $classMetadata->getClassName();
+        $this->_conn = $em->getConnection();
+        $this->_classMetadata = $classMetadata;
+        $this->_nullObject = Doctrine_ORM_Internal_Null::$INSTANCE;
+    }
+    
+    /**
+     * Inserts an entity.
+     *
+     * @param Doctrine::ORM::Entity $entity The entity to insert.
+     * @return void
+     */
+    public function insert(Doctrine_Entity $entity)
+    {
+        $insertData = array();
+        $class = $entity->getClass();
+        
+        $referenceChangeSet = $entity->_getReferenceChangeSet();
+        foreach ($referenceChangeSet as $field => $change) {
+            list($old, $new) = each($change);
+            $assocMapping = $class->getAssociationMapping($field);
+            if ( ! $assocMapping->isOneToOne() || 
$assocMapping->isInverseSide()) {
+                //echo "NOT TO-ONE OR INVERSE!";
+                continue;
+            }
+
+            foreach ($assocMapping->getSourceToTargetKeyColumns() as 
$sourceColumn => $targetColumn) {
+                //TODO: What if both join columns (local/foreign) are just 
db-only
+                // columns (no fields in models) ? Currently we assume the 
foreign column
+                // is mapped to a field in the foreign entity.
+                //TODO: throw exc if field not set
+                $insertData[$sourceColumn] = $new->_internalGetField(
+                        $new->getClass()->getFieldName($targetColumn)
+                        );
+            }
+            //...
+        }
+        
+        $this->_prepareData($entity, $insertData, true);
+        
+        //TODO: perform insert
+        $this->_conn->insert($class->getTableName(), $insertData);
+    }
+    
+    /*protected function _fillJoinColumns($entity, array &$data)
+    {
+        $referenceChangeSet = $entity->_getReferenceChangeSet();
+        foreach ($referenceChangeSet as $field => $change) {
+            list($old, $new) = each($change);
+            $assocMapping = $entity->getClass()->getAssociationMapping($field);
+            if ( ! $assocMapping->isOneToOne() || 
$assocMapping->isInverseSide()) {
+                //echo "NOT TO-ONE OR INVERSE!";
+                continue;
+            }
+
+            foreach ($assocMapping->getSourceToTargetKeyColumns() as 
$sourceColumn => $targetColumn) {
+                //TODO: What if both join columns (local/foreign) are just 
db-only
+                // columns (no fields in models) ? Currently we assume the 
foreign column
+                // is mapped to a field in the foreign entity.
+                $insertData[$sourceColumn] = $new->_internalGetField(
+                        $new->getClass()->getFieldName($targetColumn)
+                        );
+            }
+        }
+    }*/
+    
+    /**
+     * Updates an entity.
+     *
+     * @param Doctrine::ORM::Entity $entity The entity to update.
+     * @return void
+     */
+    public function update(Doctrine_Entity $entity)
+    {
+        $dataChangeSet = $entity->_getDataChangeSet();
+        $referenceChangeSet = $entity->_getReferenceChangeSet();
+        
+        foreach ($referenceChangeSet as $field => $change) {
+            $assocMapping = $entity->getClass()->getAssociationMapping($field);
+            if ($assocMapping instanceof Doctrine_Association_OneToOneMapping) 
{
+                if ($assocMapping->isInverseSide()) {
+                    continue; // ignore inverse side
+                }
+                // ... null out the foreign key
+                
+            }
+            //...
+        }
+        
+        //TODO: perform update
+    }
+    
+    /**
+     * Deletes an entity.
+     *
+     * @param Doctrine::ORM::Entity $entity The entity to delete.
+     * @return void
+     */
+    public function delete(Doctrine_Entity $entity)
+    {
+        //TODO: perform delete
+    }
+    
+    /**
+     * Inserts a row into a table.
+     *
+     * @todo This method could be used to allow mapping to secondary table(s).
+     * @see 
http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable
+     */
+    protected function _insertRow($tableName, array $data)
+    {
+        $this->_conn->insert($tableName, $data);
+    }
+    
+    /**
+     * Deletes rows of a table.
+     *
+     * @todo This method could be used to allow mapping to secondary table(s).
+     * @see 
http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable
+     */
+    protected function _deleteRow($tableName, array $identifierToMatch)
+    {
+        $this->_conn->delete($tableName, $identifierToMatch);
+    }
+    
+    /**
+     * Deletes rows of a table.
+     *
+     * @todo This method could be used to allow mapping to secondary table(s).
+     * @see 
http://www.oracle.com/technology/products/ias/toplink/jpa/resources/toplink-jpa-annotations.html#SecondaryTable
+     */
+    protected function _updateRow($tableName, array $data, array 
$identifierToMatch)
+    {
+        $this->_conn->update($tableName, $data, $identifierToMatch);
+    }
+    
+    public function getClassMetadata()
+    {
+        return $this->_classMetadata;
+    }
+    
+    /**
+     * @todo Move to ClassMetadata?
+     */
+    public function getFieldNames()
+    {
+        if ($this->_fieldNames) {
+            return $this->_fieldNames;
+        }
+        $this->_fieldNames = $this->_classMetadata->getFieldNames();
+        return $this->_fieldNames;
+    }
+    
+    /**
+     * @todo Move to ClassMetadata?
+     */
+    public function getOwningClass($fieldName)
+    {
+        return $this->_classMetadata;
+    }
+    
+    /**
+     * Callback that is invoked during the SQL construction process.
+     * @todo Move to ClassMetadata?
+     */
+    public function getCustomJoins()
+    {
+        return array();
+    }
+    
+    /**
+     * Callback that is invoked during the SQL construction process.
+     * @todo Move to ClassMetadata?
+     */
+    public function getCustomFields()
+    {
+        return array();
+    }
+    
+    /**
+     * Assumes that the keys of the given field array are field names and 
converts
+     * them to column names.
+     *
+     * @return array
+     */
+    /*protected function _convertFieldToColumnNames(array $fields, 
Doctrine_ClassMetadata $class)
+    {
+        $converted = array();
+        foreach ($fields as $fieldName => $value) {
+            $converted[$class->getColumnName($fieldName)] = $value;
+        }
+        
+        return $converted;
+    }*/
+    
+    /**
+     * Returns an array of modified fields and values with data preparation
+     * adds column aggregation inheritance and converts Records into primary 
key values
+     *
+     * @param array $array
+     * @return void
+     * @todo Move to EntityPersister. There call _getChangeSet() and apply 
this logic.
+     */
+    protected function _prepareData($entity, array &$result, $isInsert = false)
+    {
+        foreach ($entity->_getDataChangeSet() as $field => $change) {
+            list ($oldVal, $newVal) = each($change);
+            $type = $entity->getClass()->getTypeOfField($field);
+            $columnName = $entity->getClass()->getColumnName($field);
+
+            if ($newVal === Doctrine_ORM_Internal_Null::$INSTANCE) {
+                $result[$columnName] = null;
+                continue;
+            }
+
+            switch ($type) {
+                case 'array':
+                case 'object':
+                    $result[$columnName] = serialize($newVal);
+                    break;
+                case 'gzip':
+                    $result[$columnName] = gzcompress($newVal, 5);
+                    break;
+                case 'boolean':
+                    $result[$columnName] = 
$this->_em->getConnection()->convertBooleans($newVal);
+                break;
+                default:
+                    $result[$columnName] = $newVal;
+            }
+            /*$result[$columnName] = $type->convertToDatabaseValue(
+                    $newVal, 
$this->_em->getConnection()->getDatabasePlatform());*/
+        }
+        
+        // @todo Cleanup
+        // populates the discriminator column on insert in Single & Class 
Table Inheritance
+        if ($isInsert && ($entity->getClass()->isInheritanceTypeJoined() ||
+                $entity->getClass()->isInheritanceTypeSingleTable())) {
+            $discColumn = 
$entity->getClass()->getInheritanceOption('discriminatorColumn');
+            $discMap = 
$entity->getClass()->getInheritanceOption('discriminatorMap');
+            $result[$discColumn] = array_search($this->_entityName, $discMap);
+        }
+    }
+
+    
+    
+    #############################################################
+   
+    # The following is old code that needs to be removed/ported
+    
+    
+    /**
+     * deletes all related composites
+     * this method is always called internally when a record is deleted
+     *
+     * @throws PDOException         if something went wrong at database level
+     * @return void
+     */
+    protected function _deleteComposites(Doctrine_Entity $record)
+    {
+        $classMetadata = $this->_classMetadata;
+        foreach ($classMetadata->getRelations() as $fk) {
+            if ($fk->isComposite()) {
+                $obj = $record->get($fk->getAlias());
+                if ($obj instanceof Doctrine_Entity && 
+                        $obj->_state() != Doctrine_Entity::STATE_LOCKED)  {
+                    $obj->delete($this->_mapper->getConnection());
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the connection the mapper is currently using.
+     *
+     * @return Doctrine_Connection|null  The connection object.
+     */
+    public function getConnection()
+    {
+        return $this->_conn;
+    }
+    
+    public function getEntityManager()
+    {
+        return $this->_em;
+    }
+
+    /**
+     * getComponentName
+     *
+     * @return void
+     * @deprecated Use getMappedClassName()
+     */
+    public function getComponentName()
+    {
+        return $this->_domainClassName;
+    }
+    
+    /**
+     * Saves an entity.
+     *
+     * @param Doctrine_Entity $record    The entity to save.
+     * @param Doctrine_Connection $conn  The connection to use. Will default 
to the mapper's
+     *                                   connection.
+     */
+    public function save(Doctrine_Entity $record)
+    {
+        if ( ! ($record instanceof $this->_domainClassName)) {
+            throw new Doctrine_Mapper_Exception("Mapper of type " . 
$this->_domainClassName . " 
+                    can't save instances of type" . get_class($record) . ".");
+        }
+        
+        if ($conn === null) {
+            $conn = $this->_conn;
+        }
+
+        $state = $record->_state();
+        if ($state === Doctrine_Entity::STATE_LOCKED) {
+            return false;
+        }
+        
+        $record->_state(Doctrine_Entity::STATE_LOCKED);
+        
+        try {
+            $conn->beginInternalTransaction();
+            $saveLater = $this->_saveRelated($record);
+
+            $record->_state($state);
+
+            if ($record->isValid()) {
+                $this->_insertOrUpdate($record);
+            } else {
+                $conn->getTransaction()->addInvalid($record);
+            }
+
+            $state = $record->_state();
+            $record->_state(Doctrine_Entity::STATE_LOCKED);
+
+            foreach ($saveLater as $fk) {
+                $alias = $fk->getAlias();
+                if ($record->hasReference($alias)) {
+                    $obj = $record->$alias;
+                    // check that the related object is not an instance of 
Doctrine_Null
+                    if ( ! ($obj instanceof Doctrine_Null)) {
+                        $obj->save($conn);
+                    }
+                }
+            }
+
+            // save the MANY-TO-MANY associations
+            $this->saveAssociations($record);
+            // reset state
+            $record->_state($state);
+            $conn->commit();
+        } catch (Exception $e) {
+            $conn->rollback();
+            throw $e;
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Inserts or updates an entity, depending on it's state. 
+     *
+     * @param Doctrine_Entity $record  The entity to insert/update.
+     */
+    protected function _insertOrUpdate(Doctrine_Entity $record)
+    {
+        //$record->preSave();
+        //$this->notifyEntityListeners($record, 'preSave', 
Doctrine_Event::RECORD_SAVE);
+        
+        switch ($record->_state()) {
+            case Doctrine_Entity::STATE_TDIRTY:
+                $this->_insert($record);
+                break;
+            case Doctrine_Entity::STATE_DIRTY:
+            case Doctrine_Entity::STATE_PROXY:
+                $this->_update($record);
+                break;
+            case Doctrine_Entity::STATE_CLEAN:
+            case Doctrine_Entity::STATE_TCLEAN:
+                // do nothing
+                break;
+        }
+        
+        //$record->postSave();
+        //$this->notifyEntityListeners($record, 'postSave', 
Doctrine_Event::RECORD_SAVE);
+    }
+    
+    /**
+     * saves the given record
+     *
+     * @param Doctrine_Entity $record
+     * @return void
+     */
+    public function saveSingleRecord(Doctrine_Entity $record)
+    {
+        $this->_insertOrUpdate($record);
+    }
+    
+    /**
+     * saves all related records to $record
+     *
+     * @throws PDOException         if something went wrong at database level
+     * @param Doctrine_Entity $record
+     */
+    protected function _saveRelated(Doctrine_Entity $record)
+    {
+        $saveLater = array();
+        foreach ($record->_getReferences() as $k => $v) {
+            $rel = $record->getTable()->getRelation($k);
+
+            $local = $rel->getLocal();
+            $foreign = $rel->getForeign();
+
+            if ($rel instanceof Doctrine_Relation_ForeignKey) {
+                $saveLater[$k] = $rel;
+            } else if ($rel instanceof Doctrine_Relation_LocalKey) {
+                // ONE-TO-ONE relationship
+                $obj = $record->get($rel->getAlias());
+
+                // Protection against infinite function recursion before 
attempting to save
+                if ($obj instanceof Doctrine_Entity && $obj->isModified()) {
+                    $obj->save();
+                    
+                    /** Can this be removed?
+                    $id = array_values($obj->identifier());
+
+                    foreach ((array) $rel->getLocal() as $k => $field) {
+                        $record->set($field, $id[$k]);
+                    }
+                    */
+                }
+            }
+        }
+
+        return $saveLater;
+    }
+    
+    /**
+     * saveAssociations
+     *
+     * this method takes a diff of one-to-many / many-to-many original and
+     * current collections and applies the changes
+     *
+     * for example if original many-to-many related collection has records with
+     * primary keys 1,2 and 3 and the new collection has records with primary 
keys
+     * 3, 4 and 5, this method would first destroy the associations to 1 and 2 
and then
+     * save new associations to 4 and 5
+     *
+     * @throws Doctrine_Connection_Exception         if something went wrong 
at database level
+     * @param Doctrine_Entity $record
+     * @return void
+     */
+    public function saveAssociations(Doctrine_Entity $record)
+    {
+        foreach ($record->_getReferences() as $relationName => $relatedObject) 
{
+            if ($relatedObject === Doctrine_Null::$INSTANCE) {
+                continue;
+            }
+            $rel = $record->getTable()->getRelation($relationName);
+            
+            if ($rel instanceof Doctrine_Relation_Association) {
+                $relatedObject->save($this->_conn);
+                $assocTable = $rel->getAssociationTable();
+                
+                foreach ($relatedObject->getDeleteDiff() as $r) {
+                    $query = 'DELETE FROM ' . $assocTable->getTableName()
+                           . ' WHERE ' . $rel->getForeign() . ' = ?'
+                           . ' AND ' . $rel->getLocal() . ' = ?';
+                    // FIXME: composite key support
+                    $ids1 = $r->identifier();
+                    $id1 = count($ids1) > 0 ? array_pop($ids1) : null;
+                    $ids2 = $record->identifier();
+                    $id2 = count($ids2) > 0 ? array_pop($ids2) : null;
+                    $this->_conn->execute($query, array($id1, $id2));
+                }
+                
+                $assocMapper = 
$this->_conn->getMapper($assocTable->getComponentName());
+                foreach ($relatedObject->getInsertDiff() as $r)  {    
+                    $assocRecord = $assocMapper->create();
+                    
$assocRecord->set($assocTable->getFieldName($rel->getForeign()), $r);
+                    
$assocRecord->set($assocTable->getFieldName($rel->getLocal()), $record);
+                    $assocMapper->save($assocRecord);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Updates an entity.
+     *
+     * @param Doctrine_Entity $record   record to be updated
+     * @return boolean                  whether or not the update was 
successful
+     * @todo Move to Doctrine_Table (which will become Doctrine_Mapper).
+     */
+    protected function _update(Doctrine_Entity $record)
+    {
+        $record->preUpdate();
+        $this->notifyEntityListeners($record, 'preUpdate', 
Doctrine_Event::RECORD_UPDATE);
+        
+        $table = $this->_classMetadata;
+        $this->_doUpdate($record);
+        
+        $record->postUpdate();
+        $this->notifyEntityListeners($record, 'postUpdate', 
Doctrine_Event::RECORD_UPDATE);
+
+        return true;
+    }
+    
+    abstract protected function _doUpdate(Doctrine_Entity $entity);
+    
+    /**
+     * Inserts an entity.
+     *
+     * @param Doctrine_Entity $record   record to be inserted
+     * @return boolean
+     */
+    protected function _insert(Doctrine_Entity $record)
+    {
+        $record->preInsert();
+        $this->notifyEntityListeners($record, 'preInsert', 
Doctrine_Event::RECORD_INSERT);
+
+        $this->_doInsert($record);
+        $this->addRecord($record);
+        
+        $record->postInsert();
+        $this->notifyEntityListeners($record, 'postInsert', 
Doctrine_Event::RECORD_INSERT);
+        
+        return true;
+    }
+    
+    abstract protected function _doInsert(Doctrine_Entity $entity);
+    
+    /**
+     * Deletes given entity and all it's related entities.
+     *
+     * Triggered Events: onPreDelete, onDelete.
+     *
+     * @return boolean      true on success, false on failure
+     * @throws Doctrine_Mapper_Exception
+     */
+    public function delete_old(Doctrine_Entity $record)
+    {
+        if ( ! $record->exists()) {
+            return false;
+        }
+        
+        if ( ! ($record instanceof $this->_domainClassName)) {
+            throw new Doctrine_Mapper_Exception("Mapper of type " . 
$this->_domainClassName . " 
+                    can't save instances of type" . get_class($record) . ".");
+        }
+        
+        if ($conn == null) {
+            $conn = $this->_conn;
+        }
+
+        $record->preDelete();
+        $this->notifyEntityListeners($record, 'preDelete', 
Doctrine_Event::RECORD_DELETE);
+        
+        $table = $this->_classMetadata;
+
+        $state = $record->_state();
+        $record->_state(Doctrine_Entity::STATE_LOCKED);
+        
+        $this->_doDelete($record);
+        
+        $record->postDelete();
+        $this->notifyEntityListeners($record, 'postDelete', 
Doctrine_Event::RECORD_DELETE);
+
+        return true;
+    }
+    
+
+}

Added: trunk/lib/Doctrine/ORM/Persisters/Exception.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/Exception.php                             
(rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/Exception.php     2008-09-12 09:44:51 UTC 
(rev 4921)
@@ -0,0 +1,38 @@
+<?php
+/*
+ *  $Id$
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the LGPL. For more information, see
+ * <http://www.phpdoctrine.org>.
+ */
+
+#namespace Doctrine::ORM;
+
+/**
+ * Doctrine_EntityPersister_Exception
+ *
+ * @package     Doctrine
+ * @subpackage  Entity
+ * @author      Konsta Vesterinen <[EMAIL PROTECTED]>
+ * @author      Roman Borschel <[EMAIL PROTECTED]>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @link        www.phpdoctrine.org
+ * @since       2.0
+ * @version     $Revision$
+ */
+class Doctrine_EntityPersister_Exception extends Doctrine_Exception
+{
+}
\ No newline at end of file

Added: trunk/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php               
                (rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php       
2008-09-12 09:44:51 UTC (rev 4921)
@@ -0,0 +1,279 @@
+<?php 
+/*
+ *  $Id$
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the LGPL. For more information, see
+ * <http://www.phpdoctrine.org>.
+ */
+
+/**
+ * The joined subclass persister maps a single entity instance to several 
tables in the
+ * database as it is defined by <tt>Class Table Inheritance</tt>.
+ *
+ * @author      Roman Borschel <[EMAIL PROTECTED]>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @version     $Revision$
+ * @link        www.phpdoctrine.org
+ * @since       2.0
+ */
+class Doctrine_ORM_Persisters_JoinedSubclassPersister extends 
Doctrine_ORM_Persisters_AbstractEntityPersister
+{    
+    /**
+     * Inserts an entity that is part of a Class Table Inheritance hierarchy.
+     *
+     * @param Doctrine_Entity $record   record to be inserted
+     * @return boolean
+     * @override
+     */
+    public function insert(Doctrine_Entity $entity)
+    {
+        $class = $entity->getClass();
+        
+        $dataSet = array();
+        
+        $this->_prepareData($entity, $dataSet, true);
+        
+        $dataSet = $this->_groupFieldsByDefiningClass($class, $dataSet);
+        
+        $component = $class->getClassName();
+        $classes = $class->getParentClasses();
+        array_unshift($classes, $component);
+        
+        $identifier = null;
+        foreach (array_reverse($classes) as $k => $parent) {
+            $parentClass = $this->_em->getClassMetadata($parent);
+            if ($k == 0) {
+                if ($parentClass->isIdGeneratorIdentity()) {
+                    $this->_insertRow($parentClass->getTableName(), 
$dataSet[$parent]);
+                    $identifier = $this->_conn->lastInsertId();
+                } else if ($parentClass->isIdGeneratorSequence()) {
+                    $seq = 
$entity->getClassMetadata()->getTableOption('sequenceName');
+                    if ( ! empty($seq)) {
+                        $id = $this->_conn->getSequenceManager()->nextId($seq);
+                        $identifierFields = $parentClass->getIdentifier();
+                        $dataSet[$parent][$identifierFields[0]] = $id;
+                        $this->_insertRow($parentClass->getTableName(), 
$dataSet[$parent]);
+                    }
+                } else {
+                    throw new Doctrine_Mapper_Exception("Unsupported 
identifier type '$identifierType'.");
+                }
+                $entity->_assignIdentifier($identifier);
+            } else {
+                foreach ($entity->_identifier() as $id => $value) {
+                    $dataSet[$parent][$parentClass->getColumnName($id)] = 
$value;
+                }
+                $this->_insertRow($parentClass->getTableName(), 
$dataSet[$parent]);
+            }
+        }
+
+        return true;
+    }
+    
+    /**
+     * Updates an entity that is part of a Class Table Inheritance hierarchy.
+     *
+     * @param Doctrine_Entity $record   record to be updated
+     * @return boolean                  whether or not the update was 
successful
+     */
+    protected function _doUpdate(Doctrine_Entity $record)
+    {
+        $conn = $this->_conn;
+        $classMetadata = $this->_classMetadata;
+        $identifier = $this->_convertFieldToColumnNames($record->identifier(), 
$classMetadata);
+        $dataSet = $this->_groupFieldsByDefiningClass($record);
+        $component = $classMetadata->getClassName();
+        $classes = $classMetadata->getParentClasses();
+        array_unshift($classes, $component);
+
+        foreach ($record as $field => $value) {
+            if ($value instanceof Doctrine_Entity) {
+                if ( ! $value->exists()) {
+                    $value->save();
+                }
+                $idValues = $value->identifier();
+                $record->set($field, $idValues[0]);
+            }
+        }
+
+        foreach (array_reverse($classes) as $class) {
+            $parentTable = $conn->getClassMetadata($class);
+            $this->_updateRow($parentTable->getTableName(), $dataSet[$class], 
$identifier);
+        }
+        
+        $record->assignIdentifier(true);
+
+        return true;
+    }
+    
+    /**
+     * Deletes an entity that is part of a Class Table Inheritance hierarchy.
+     *
+     */
+    protected function _doDelete(Doctrine_Entity $record)
+    {
+        $conn = $this->_conn;
+        try {
+            $class = $this->_classMetadata;
+            $conn->beginInternalTransaction();
+            $this->_deleteComposites($record);
+
+            $record->_state(Doctrine_Entity::STATE_TDIRTY);
+
+            $identifier = 
$this->_convertFieldToColumnNames($record->identifier(), $class);
+            
+            // run deletions, starting from the class, upwards the hierarchy
+            $conn->delete($class->getTableName(), $identifier);
+            foreach ($class->getParentClasses() as $parent) {
+                $parentClass = $conn->getClassMetadata($parent);
+                $this->_deleteRow($parentClass->getTableName(), $identifier);
+            }
+            
+            $record->_state(Doctrine_Entity::STATE_TCLEAN);
+
+            $this->removeRecord($record); // @todo should be done in the 
unitofwork
+            $conn->commit();
+        } catch (Exception $e) {
+            $conn->rollback();
+            throw $e;
+        }
+
+        return true;
+    }
+    
+    /**
+     * Adds all parent classes as INNER JOINs and subclasses as OUTER JOINs
+     * to the query.
+     *
+     * Callback that is invoked during the SQL construction process.
+     *
+     * @return array  The custom joins in the format <className> => <joinType>
+     */
+    public function getCustomJoins()
+    {
+        $customJoins = array();
+        $classMetadata = $this->_classMetadata;
+        foreach ($classMetadata->getParentClasses() as $parentClass) {
+            $customJoins[$parentClass] = 'INNER';
+        }
+        foreach ($classMetadata->getSubclasses() as $subClass) {
+            if ($subClass != $this->getComponentName()) {
+                $customJoins[$subClass] = 'LEFT';
+            }
+        }
+        
+        return $customJoins;
+    }
+    
+    /**
+     * Adds the discriminator column to the selected fields in a query as well 
as
+     * all fields of subclasses. In Class Table Inheritance the default 
behavior is that
+     * all subclasses are joined in through OUTER JOINs when querying a base 
class.
+     *
+     * Callback that is invoked during the SQL construction process.
+     *
+     * @return array  An array with the field names that will get added to the 
query.
+     */
+    public function getCustomFields()
+    {
+        $classMetadata = $this->_classMetadata;
+        $conn = $this->_conn;
+        $fields = 
array($classMetadata->getInheritanceOption('discriminatorColumn'));
+        if ($classMetadata->getSubclasses()) {
+            foreach ($classMetadata->getSubclasses() as $subClass) {
+                $fields = 
array_merge($conn->getClassMetadata($subClass)->getFieldNames(), $fields);
+            }
+        }
+        
+        return array_unique($fields);
+    }
+    
+    /**
+     *
+     */
+    public function getFieldNames()
+    {
+        if ($this->_fieldNames) {
+            return $this->_fieldNames;
+        }
+        
+        $fieldNames = $this->_classMetadata->getFieldNames();
+        $this->_fieldNames = array_unique($fieldNames);
+        
+        return $fieldNames;
+    }
+    
+    /**
+     * 
+     * @todo Looks like this better belongs into the ClassMetadata class.
+     */
+    public function getOwningClass($fieldName)
+    {
+        $conn = $this->_conn;
+        $classMetadata = $this->_classMetadata;
+        if ($classMetadata->hasField($fieldName) && ! 
$classMetadata->isInheritedField($fieldName)) {
+            return $classMetadata;
+        }
+        
+        foreach ($classMetadata->getParentClasses() as $parentClass) {
+            $parentTable = $conn->getClassMetadata($parentClass);
+            if ($parentTable->hasField($fieldName) && ! 
$parentTable->isInheritedField($fieldName)) {
+                return $parentTable;
+            }
+        }
+        
+        foreach ((array)$classMetadata->getSubclasses() as $subClass) {
+            $subTable = $conn->getClassMetadata($subClass);
+            if ($subTable->hasField($fieldName) && ! 
$subTable->isInheritedField($fieldName)) {
+                return $subTable;
+            }
+        }
+        
+        throw new Doctrine_Mapper_Exception("Unable to find defining class of 
field '$fieldName'.");
+    }
+    
+    /**
+     * Analyzes the fields of the entity and creates a map in which the field 
names
+     * are grouped by the class names they belong to. 
+     *
+     * @return array
+     */
+    protected function _groupFieldsByDefiningClass(Doctrine_ClassMetadata 
$class, array $fields)
+    {
+        $dataSet = array();
+        $component = $class->getClassName();
+        
+        $classes = array_merge(array($component), $class->getParentClasses());
+        
+        foreach ($classes as $class) {
+            $dataSet[$class] = array();            
+            $parentClassMetadata = $this->_em->getClassMetadata($class);
+            foreach ($parentClassMetadata->getFieldMappings() as $fieldName => 
$mapping) {
+                if ((isset($mapping['id']) && $mapping['id'] === true) ||
+                        (isset($mapping['inherited']) && $mapping['inherited'] 
=== true)) {
+                    continue;
+                }
+                if ( ! array_key_exists($fieldName, $fields)) {
+                    continue;
+                }
+                $columnName = $parentClassMetadata->getColumnName($fieldName);
+                $dataSet[$class][$columnName] = $fields[$fieldName];
+            }
+        }
+        
+        return $dataSet;
+    }
+}
+

Added: trunk/lib/Doctrine/ORM/Persisters/SingleTablePersister.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/SingleTablePersister.php                  
        (rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/SingleTablePersister.php  2008-09-12 
09:44:51 UTC (rev 4921)
@@ -0,0 +1,3 @@
+<?php
+
+?>
\ No newline at end of file

Added: trunk/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php               
                (rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/StandardEntityPersister.php       
2008-09-12 09:44:51 UTC (rev 4921)
@@ -0,0 +1,117 @@
+<?php
+/*
+ *  $Id$
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the LGPL. For more information, see
+ * <http://www.phpdoctrine.org>.
+ */
+
+/**
+ * The default persister strategy maps a single entity instance to a single 
database table,
+ * as is the case in Single Table Inheritance & Concrete Table Inheritance.
+ *
+ * @author      Roman Borschel <[EMAIL PROTECTED]>
+ * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
+ * @version     $Revision$
+ * @link        www.phpdoctrine.org
+ * @since       2.0
+ */
+class Doctrine_ORM_Persisters_StandardEntityPersister extends 
Doctrine_ORM_Persisters_AbstractEntityPersister
+{
+    /**
+     * Deletes an entity.
+     */
+    protected function _doDelete(Doctrine_Entity $record)
+    {
+        $conn = $this->_conn;
+        $metadata = $this->_classMetadata;
+        try {
+            $conn->beginInternalTransaction();
+            $this->_deleteComposites($record);
+
+            $record->_state(Doctrine_Entity::STATE_TDIRTY);
+            
+            $identifier = 
$this->_convertFieldToColumnNames($record->identifier(), $metadata);
+            $this->_deleteRow($metadata->getTableName(), $identifier);
+            $record->_state(Doctrine_Entity::STATE_TCLEAN);
+
+            $this->removeRecord($record);
+            $conn->commit();
+        } catch (Exception $e) {
+            $conn->rollback();
+            throw $e;
+        }
+    }
+    
+    /**
+     * Inserts a single entity into the database, without any related entities.
+     *
+     * @param Doctrine_Entity $record   The entity to insert.
+     */
+    protected function _doInsert(Doctrine_Entity $record)
+    {
+        $conn = $this->_conn;
+        
+        $fields = $record->getPrepared();
+        if (empty($fields)) {
+            return false;
+        }
+        
+        $class = $this->_classMetadata;
+        $identifier = $class->getIdentifier();
+        $fields = $this->_convertFieldToColumnNames($fields, $class);
+
+        $seq = $class->getTableOption('sequenceName');
+        if ( ! empty($seq)) {
+            $id = $conn->sequence->nextId($seq);
+            $seqName = $identifier[0];
+            $fields[$seqName] = $id;
+            $record->assignIdentifier($id);
+        }
+        
+        $this->_insertRow($class->getTableName(), $fields);
+
+        if (empty($seq) && count($identifier) == 1 &&
+                $class->getIdentifierType() != Doctrine::IDENTIFIER_NATURAL) {
+            if (strtolower($conn->getName()) == 'pgsql') {
+                $seq = $class->getTableName() . '_' . $identifier[0];
+            }
+
+            $id = $conn->sequence->lastInsertId($seq);
+
+            if ( ! $id) {
+                throw new Doctrine_Mapper_Exception("Couldn't get last insert 
identifier.");
+            }
+
+            $record->assignIdentifier($id);
+        } else {
+            $record->assignIdentifier(true);
+        }
+    }
+    
+    /**
+     * Updates an entity.
+     */
+    protected function _doUpdate(Doctrine_Entity $record)
+    {
+        $conn = $this->_conn;
+        $classMetadata = $this->_classMetadata;
+        $identifier = $this->_convertFieldToColumnNames($record->identifier(), 
$classMetadata);
+        $data = $this->_convertFieldToColumnNames($record->getPrepared(), 
$classMetadata);
+        $this->_updateRow($classMetadata->getTableName(), $data, $identifier);
+        $record->assignIdentifier(true);
+    }
+}
\ No newline at end of file

Added: trunk/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php
===================================================================
--- trunk/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php                
                (rev 0)
+++ trunk/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php        
2008-09-12 09:44:51 UTC (rev 4921)
@@ -0,0 +1,3 @@
+<?php
+
+?>
\ No newline at end of file

Modified: trunk/tests/Orm/EntityPersisterTest.php
===================================================================
--- trunk/tests/Orm/EntityPersisterTest.php     2008-09-12 09:39:43 UTC (rev 
4920)
+++ trunk/tests/Orm/EntityPersisterTest.php     2008-09-12 09:44:51 UTC (rev 
4921)
@@ -23,7 +23,7 @@
         $this->_classMetadataMock = new 
Doctrine_ClassMetadataMock("ForumUser", $this->_emMock);
         $this->_classMetadataMock->setIdGenerator($this->_idGenMock);
         $this->_connMock->setDatabasePlatform(new 
Doctrine_DatabasePlatformMock());        
-        $this->_persister = new Doctrine_EntityPersister_Standard(
+        $this->_persister = new 
Doctrine_ORM_Persisters_StandardEntityPersister(
                 $this->_emMock, $this->_emMock->getClassMetadata("ForumUser"));
                 
         $this->_emMock->activate();

Modified: trunk/tests/lib/mocks/Doctrine_EntityPersisterMock.php
===================================================================
--- trunk/tests/lib/mocks/Doctrine_EntityPersisterMock.php      2008-09-12 
09:39:43 UTC (rev 4920)
+++ trunk/tests/lib/mocks/Doctrine_EntityPersisterMock.php      2008-09-12 
09:44:51 UTC (rev 4921)
@@ -1,6 +1,6 @@
 <?php
 
-class Doctrine_EntityPersisterMock extends Doctrine_EntityPersister_Standard
+class Doctrine_EntityPersisterMock extends 
Doctrine_ORM_Persisters_StandardEntityPersister
 {
     private $_inserts = array();
     private $_updates = array();


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"doctrine-svn" group.
 To post to this group, send email to [email protected]
 To unsubscribe from this group, send email to [EMAIL PROTECTED]
 For more options, visit this group at 
http://groups.google.co.uk/group/doctrine-svn?hl=en-GB
-~----------~----~----~----~------~----~------~--~---

Reply via email to