vendor/doctrine/lib/Doctrine/ORM/UnitOfWork.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     1 <?php
       
     2 /*
       
     3  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
       
     4  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
       
     5  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
       
     6  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
       
     7  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
       
     8  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
       
     9  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
       
    10  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
       
    11  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    12  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
       
    13  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    14  *
       
    15  * This software consists of voluntary contributions made by many individuals
       
    16  * and is licensed under the LGPL. For more information, see
       
    17  * <http://www.doctrine-project.org>.
       
    18  */
       
    19 
       
    20 namespace Doctrine\ORM;
       
    21 
       
    22 use Exception, InvalidArgumentException, UnexpectedValueException,
       
    23     Doctrine\Common\Collections\ArrayCollection,
       
    24     Doctrine\Common\Collections\Collection,
       
    25     Doctrine\Common\NotifyPropertyChanged,
       
    26     Doctrine\Common\PropertyChangedListener,
       
    27     Doctrine\ORM\Event\LifecycleEventArgs,
       
    28     Doctrine\ORM\Mapping\ClassMetadata,
       
    29     Doctrine\ORM\Proxy\Proxy;
       
    30 
       
    31 /**
       
    32  * The UnitOfWork is responsible for tracking changes to objects during an
       
    33  * "object-level" transaction and for writing out changes to the database
       
    34  * in the correct order.
       
    35  *
       
    36  * @since       2.0
       
    37  * @author      Benjamin Eberlei <kontakt@beberlei.de>
       
    38  * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
       
    39  * @author      Jonathan Wage <jonwage@gmail.com>
       
    40  * @author      Roman Borschel <roman@code-factory.org>
       
    41  * @internal    This class contains highly performance-sensitive code.
       
    42  */
       
    43 class UnitOfWork implements PropertyChangedListener
       
    44 {
       
    45     /**
       
    46      * An entity is in MANAGED state when its persistence is managed by an EntityManager.
       
    47      */
       
    48     const STATE_MANAGED = 1;
       
    49 
       
    50     /**
       
    51      * An entity is new if it has just been instantiated (i.e. using the "new" operator)
       
    52      * and is not (yet) managed by an EntityManager.
       
    53      */
       
    54     const STATE_NEW = 2;
       
    55 
       
    56     /**
       
    57      * A detached entity is an instance with persistent state and identity that is not
       
    58      * (or no longer) associated with an EntityManager (and a UnitOfWork).
       
    59      */
       
    60     const STATE_DETACHED = 3;
       
    61 
       
    62     /**
       
    63      * A removed entity instance is an instance with a persistent identity,
       
    64      * associated with an EntityManager, whose persistent state will be deleted
       
    65      * on commit.
       
    66      */
       
    67     const STATE_REMOVED = 4;
       
    68 
       
    69     /**
       
    70      * The identity map that holds references to all managed entities that have
       
    71      * an identity. The entities are grouped by their class name.
       
    72      * Since all classes in a hierarchy must share the same identifier set,
       
    73      * we always take the root class name of the hierarchy.
       
    74      *
       
    75      * @var array
       
    76      */
       
    77     private $identityMap = array();
       
    78 
       
    79     /**
       
    80      * Map of all identifiers of managed entities.
       
    81      * Keys are object ids (spl_object_hash).
       
    82      *
       
    83      * @var array
       
    84      */
       
    85     private $entityIdentifiers = array();
       
    86 
       
    87     /**
       
    88      * Map of the original entity data of managed entities.
       
    89      * Keys are object ids (spl_object_hash). This is used for calculating changesets
       
    90      * at commit time.
       
    91      *
       
    92      * @var array
       
    93      * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
       
    94      *           A value will only really be copied if the value in the entity is modified
       
    95      *           by the user.
       
    96      */
       
    97     private $originalEntityData = array();
       
    98 
       
    99     /**
       
   100      * Map of entity changes. Keys are object ids (spl_object_hash).
       
   101      * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
       
   102      *
       
   103      * @var array
       
   104      */
       
   105     private $entityChangeSets = array();
       
   106 
       
   107     /**
       
   108      * The (cached) states of any known entities.
       
   109      * Keys are object ids (spl_object_hash).
       
   110      *
       
   111      * @var array
       
   112      */
       
   113     private $entityStates = array();
       
   114 
       
   115     /**
       
   116      * Map of entities that are scheduled for dirty checking at commit time.
       
   117      * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
       
   118      * Keys are object ids (spl_object_hash).
       
   119      * 
       
   120      * @var array
       
   121      * @todo rename: scheduledForSynchronization
       
   122      */
       
   123     private $scheduledForDirtyCheck = array();
       
   124 
       
   125     /**
       
   126      * A list of all pending entity insertions.
       
   127      *
       
   128      * @var array
       
   129      */
       
   130     private $entityInsertions = array();
       
   131 
       
   132     /**
       
   133      * A list of all pending entity updates.
       
   134      *
       
   135      * @var array
       
   136      */
       
   137     private $entityUpdates = array();
       
   138     
       
   139     /**
       
   140      * Any pending extra updates that have been scheduled by persisters.
       
   141      * 
       
   142      * @var array
       
   143      */
       
   144     private $extraUpdates = array();
       
   145 
       
   146     /**
       
   147      * A list of all pending entity deletions.
       
   148      *
       
   149      * @var array
       
   150      */
       
   151     private $entityDeletions = array();
       
   152 
       
   153     /**
       
   154      * All pending collection deletions.
       
   155      *
       
   156      * @var array
       
   157      */
       
   158     private $collectionDeletions = array();
       
   159 
       
   160     /**
       
   161      * All pending collection updates.
       
   162      *
       
   163      * @var array
       
   164      */
       
   165     private $collectionUpdates = array();
       
   166 
       
   167     /**
       
   168      * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
       
   169      * At the end of the UnitOfWork all these collections will make new snapshots
       
   170      * of their data.
       
   171      *
       
   172      * @var array
       
   173      */
       
   174     private $visitedCollections = array();
       
   175 
       
   176     /**
       
   177      * The EntityManager that "owns" this UnitOfWork instance.
       
   178      *
       
   179      * @var Doctrine\ORM\EntityManager
       
   180      */
       
   181     private $em;
       
   182 
       
   183     /**
       
   184      * The calculator used to calculate the order in which changes to
       
   185      * entities need to be written to the database.
       
   186      *
       
   187      * @var Doctrine\ORM\Internal\CommitOrderCalculator
       
   188      */
       
   189     private $commitOrderCalculator;
       
   190 
       
   191     /**
       
   192      * The entity persister instances used to persist entity instances.
       
   193      *
       
   194      * @var array
       
   195      */
       
   196     private $persisters = array();
       
   197 
       
   198     /**
       
   199      * The collection persister instances used to persist collections.
       
   200      *
       
   201      * @var array
       
   202      */
       
   203     private $collectionPersisters = array();
       
   204     
       
   205     /**
       
   206      * The EventManager used for dispatching events.
       
   207      * 
       
   208      * @var EventManager
       
   209      */
       
   210     private $evm;
       
   211     
       
   212     /**
       
   213      * Orphaned entities that are scheduled for removal.
       
   214      * 
       
   215      * @var array
       
   216      */
       
   217     private $orphanRemovals = array();
       
   218     
       
   219     //private $_readOnlyObjects = array();
       
   220 
       
   221     /**
       
   222      * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
       
   223      * 
       
   224      * @var array
       
   225      */
       
   226     private $eagerLoadingEntities = array();
       
   227 
       
   228     /**
       
   229      * Initializes a new UnitOfWork instance, bound to the given EntityManager.
       
   230      *
       
   231      * @param Doctrine\ORM\EntityManager $em
       
   232      */
       
   233     public function __construct(EntityManager $em)
       
   234     {
       
   235         $this->em = $em;
       
   236         $this->evm = $em->getEventManager();
       
   237     }
       
   238 
       
   239     /**
       
   240      * Commits the UnitOfWork, executing all operations that have been postponed
       
   241      * up to this point. The state of all managed entities will be synchronized with
       
   242      * the database.
       
   243      * 
       
   244      * The operations are executed in the following order:
       
   245      * 
       
   246      * 1) All entity insertions
       
   247      * 2) All entity updates
       
   248      * 3) All collection deletions
       
   249      * 4) All collection updates
       
   250      * 5) All entity deletions
       
   251      * 
       
   252      */
       
   253     public function commit()
       
   254     {
       
   255         // Compute changes done since last commit.
       
   256         $this->computeChangeSets();
       
   257 
       
   258         if ( ! ($this->entityInsertions ||
       
   259                 $this->entityDeletions ||
       
   260                 $this->entityUpdates ||
       
   261                 $this->collectionUpdates ||
       
   262                 $this->collectionDeletions ||
       
   263                 $this->orphanRemovals)) {
       
   264             return; // Nothing to do.
       
   265         }
       
   266 
       
   267         if ($this->orphanRemovals) {
       
   268             foreach ($this->orphanRemovals as $orphan) {
       
   269                 $this->remove($orphan);
       
   270             }
       
   271         }
       
   272         
       
   273         // Raise onFlush
       
   274         if ($this->evm->hasListeners(Events::onFlush)) {
       
   275             $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->em));
       
   276         }
       
   277         
       
   278         // Now we need a commit order to maintain referential integrity
       
   279         $commitOrder = $this->getCommitOrder();
       
   280 
       
   281         $conn = $this->em->getConnection();
       
   282 
       
   283         $conn->beginTransaction();
       
   284         try {
       
   285             if ($this->entityInsertions) {
       
   286                 foreach ($commitOrder as $class) {
       
   287                     $this->executeInserts($class);
       
   288                 }
       
   289             }
       
   290 
       
   291             if ($this->entityUpdates) {
       
   292                 foreach ($commitOrder as $class) {
       
   293                     $this->executeUpdates($class);
       
   294                 }
       
   295             }
       
   296 
       
   297             // Extra updates that were requested by persisters.
       
   298             if ($this->extraUpdates) {
       
   299                 $this->executeExtraUpdates();
       
   300             }
       
   301 
       
   302             // Collection deletions (deletions of complete collections)
       
   303             foreach ($this->collectionDeletions as $collectionToDelete) {
       
   304                 $this->getCollectionPersister($collectionToDelete->getMapping())
       
   305                         ->delete($collectionToDelete);
       
   306             }
       
   307             // Collection updates (deleteRows, updateRows, insertRows)
       
   308             foreach ($this->collectionUpdates as $collectionToUpdate) {
       
   309                 $this->getCollectionPersister($collectionToUpdate->getMapping())
       
   310                         ->update($collectionToUpdate);
       
   311             }
       
   312 
       
   313             // Entity deletions come last and need to be in reverse commit order
       
   314             if ($this->entityDeletions) {
       
   315                 for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
       
   316                     $this->executeDeletions($commitOrder[$i]);
       
   317                 }
       
   318             }
       
   319 
       
   320             $conn->commit();
       
   321         } catch (Exception $e) {
       
   322             $this->em->close();
       
   323             $conn->rollback();
       
   324             throw $e;
       
   325         }
       
   326 
       
   327         // Take new snapshots from visited collections
       
   328         foreach ($this->visitedCollections as $coll) {
       
   329             $coll->takeSnapshot();
       
   330         }
       
   331 
       
   332         // Clear up
       
   333         $this->entityInsertions =
       
   334         $this->entityUpdates =
       
   335         $this->entityDeletions =
       
   336         $this->extraUpdates =
       
   337         $this->entityChangeSets =
       
   338         $this->collectionUpdates =
       
   339         $this->collectionDeletions =
       
   340         $this->visitedCollections =
       
   341         $this->scheduledForDirtyCheck =
       
   342         $this->orphanRemovals = array();
       
   343     }
       
   344     
       
   345     /**
       
   346      * Executes any extra updates that have been scheduled.
       
   347      */
       
   348     private function executeExtraUpdates()
       
   349     {
       
   350         foreach ($this->extraUpdates as $oid => $update) {
       
   351             list ($entity, $changeset) = $update;
       
   352             $this->entityChangeSets[$oid] = $changeset;
       
   353             $this->getEntityPersister(get_class($entity))->update($entity);
       
   354         }
       
   355     }
       
   356 
       
   357     /**
       
   358      * Gets the changeset for an entity.
       
   359      *
       
   360      * @return array
       
   361      */
       
   362     public function getEntityChangeSet($entity)
       
   363     {
       
   364         $oid = spl_object_hash($entity);
       
   365         if (isset($this->entityChangeSets[$oid])) {
       
   366             return $this->entityChangeSets[$oid];
       
   367         }
       
   368         return array();
       
   369     }
       
   370 
       
   371     /**
       
   372      * Computes the changes that happened to a single entity.
       
   373      *
       
   374      * Modifies/populates the following properties:
       
   375      *
       
   376      * {@link _originalEntityData}
       
   377      * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
       
   378      * then it was not fetched from the database and therefore we have no original
       
   379      * entity data yet. All of the current entity data is stored as the original entity data.
       
   380      *
       
   381      * {@link _entityChangeSets}
       
   382      * The changes detected on all properties of the entity are stored there.
       
   383      * A change is a tuple array where the first entry is the old value and the second
       
   384      * entry is the new value of the property. Changesets are used by persisters
       
   385      * to INSERT/UPDATE the persistent entity state.
       
   386      *
       
   387      * {@link _entityUpdates}
       
   388      * If the entity is already fully MANAGED (has been fetched from the database before)
       
   389      * and any changes to its properties are detected, then a reference to the entity is stored
       
   390      * there to mark it for an update.
       
   391      *
       
   392      * {@link _collectionDeletions}
       
   393      * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
       
   394      * then this collection is marked for deletion.
       
   395      *
       
   396      * @param ClassMetadata $class The class descriptor of the entity.
       
   397      * @param object $entity The entity for which to compute the changes.
       
   398      */
       
   399     public function computeChangeSet(ClassMetadata $class, $entity)
       
   400     {
       
   401         if ( ! $class->isInheritanceTypeNone()) {
       
   402             $class = $this->em->getClassMetadata(get_class($entity));
       
   403         }
       
   404         
       
   405         $oid = spl_object_hash($entity);
       
   406         $actualData = array();
       
   407         foreach ($class->reflFields as $name => $refProp) {
       
   408             $value = $refProp->getValue($entity);
       
   409             if (isset($class->associationMappings[$name])
       
   410                     && ($class->associationMappings[$name]['type'] & ClassMetadata::TO_MANY)
       
   411                     && $value !== null
       
   412                     && ! ($value instanceof PersistentCollection)) {
       
   413 
       
   414                 // If $value is not a Collection then use an ArrayCollection.
       
   415                 if ( ! $value instanceof Collection) {
       
   416                     $value = new ArrayCollection($value);
       
   417                 }
       
   418                 
       
   419                 $assoc = $class->associationMappings[$name];
       
   420                 
       
   421                 // Inject PersistentCollection
       
   422                 $coll = new PersistentCollection(
       
   423                     $this->em,
       
   424                     $this->em->getClassMetadata($assoc['targetEntity']),
       
   425                     $value
       
   426                 );
       
   427                 
       
   428                 $coll->setOwner($entity, $assoc);
       
   429                 $coll->setDirty( ! $coll->isEmpty());
       
   430                 $class->reflFields[$name]->setValue($entity, $coll);
       
   431                 $actualData[$name] = $coll;
       
   432             } else if ( (! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField) ) {
       
   433                 $actualData[$name] = $value;
       
   434             }
       
   435         }
       
   436 
       
   437         if ( ! isset($this->originalEntityData[$oid])) {
       
   438             // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
       
   439             // These result in an INSERT.
       
   440             $this->originalEntityData[$oid] = $actualData;
       
   441             $changeSet = array();
       
   442             foreach ($actualData as $propName => $actualValue) {
       
   443                 if (isset($class->associationMappings[$propName])) {
       
   444                     $assoc = $class->associationMappings[$propName];
       
   445                     if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
       
   446                         $changeSet[$propName] = array(null, $actualValue);
       
   447                     }
       
   448                 } else {
       
   449                     $changeSet[$propName] = array(null, $actualValue);
       
   450                 }
       
   451             }
       
   452             $this->entityChangeSets[$oid] = $changeSet;
       
   453         } else {
       
   454             // Entity is "fully" MANAGED: it was already fully persisted before
       
   455             // and we have a copy of the original data
       
   456             $originalData = $this->originalEntityData[$oid];
       
   457             $isChangeTrackingNotify = $class->isChangeTrackingNotify();
       
   458             $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) ? $this->entityChangeSets[$oid] : array();
       
   459 
       
   460             foreach ($actualData as $propName => $actualValue) {
       
   461                 $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
       
   462                 if (isset($class->associationMappings[$propName])) {
       
   463                     $assoc = $class->associationMappings[$propName];
       
   464                     if ($assoc['type'] & ClassMetadata::TO_ONE && $orgValue !== $actualValue) {
       
   465                         if ($assoc['isOwningSide']) {
       
   466                             $changeSet[$propName] = array($orgValue, $actualValue);
       
   467                         }
       
   468                         if ($orgValue !== null && $assoc['orphanRemoval']) {
       
   469                             $this->scheduleOrphanRemoval($orgValue);
       
   470                         }
       
   471                     } else if ($orgValue instanceof PersistentCollection && $orgValue !== $actualValue) {
       
   472                         // A PersistentCollection was de-referenced, so delete it.
       
   473                         if  ( ! in_array($orgValue, $this->collectionDeletions, true)) {
       
   474                             $this->collectionDeletions[] = $orgValue;
       
   475                             $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
       
   476                         }
       
   477                     }
       
   478                 } else if ($isChangeTrackingNotify) {
       
   479                     continue;
       
   480                 } else if ($orgValue !== $actualValue) {
       
   481                     $changeSet[$propName] = array($orgValue, $actualValue);
       
   482                 }
       
   483             }
       
   484             if ($changeSet) {
       
   485                 $this->entityChangeSets[$oid] = $changeSet;
       
   486                 $this->originalEntityData[$oid] = $actualData;
       
   487                 $this->entityUpdates[$oid] = $entity;
       
   488             }
       
   489         }
       
   490 
       
   491         // Look for changes in associations of the entity
       
   492         foreach ($class->associationMappings as $field => $assoc) {
       
   493             $val = $class->reflFields[$field]->getValue($entity);
       
   494             if ($val !== null) {
       
   495                 $this->computeAssociationChanges($assoc, $val);
       
   496             }
       
   497         }
       
   498     }
       
   499 
       
   500     /**
       
   501      * Computes all the changes that have been done to entities and collections
       
   502      * since the last commit and stores these changes in the _entityChangeSet map
       
   503      * temporarily for access by the persisters, until the UoW commit is finished.
       
   504      */
       
   505     public function computeChangeSets()
       
   506     {
       
   507         // Compute changes for INSERTed entities first. This must always happen.
       
   508         foreach ($this->entityInsertions as $entity) {
       
   509             $class = $this->em->getClassMetadata(get_class($entity));
       
   510             $this->computeChangeSet($class, $entity);
       
   511         }
       
   512 
       
   513         // Compute changes for other MANAGED entities. Change tracking policies take effect here.
       
   514         foreach ($this->identityMap as $className => $entities) {
       
   515             $class = $this->em->getClassMetadata($className);
       
   516 
       
   517             // Skip class if instances are read-only
       
   518             if ($class->isReadOnly) {
       
   519                 continue;
       
   520             }
       
   521 
       
   522             // If change tracking is explicit or happens through notification, then only compute
       
   523             // changes on entities of that type that are explicitly marked for synchronization.
       
   524             $entitiesToProcess = ! $class->isChangeTrackingDeferredImplicit() ?
       
   525                     (isset($this->scheduledForDirtyCheck[$className]) ?
       
   526                         $this->scheduledForDirtyCheck[$className] : array())
       
   527                     : $entities;
       
   528 
       
   529             foreach ($entitiesToProcess as $entity) {
       
   530                 // Ignore uninitialized proxy objects
       
   531                 if (/* $entity is readOnly || */ $entity instanceof Proxy && ! $entity->__isInitialized__) {
       
   532                     continue;
       
   533                 }
       
   534                 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
       
   535                 $oid = spl_object_hash($entity);
       
   536                 if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) {
       
   537                     $this->computeChangeSet($class, $entity);
       
   538                 }
       
   539             }
       
   540         }
       
   541     }
       
   542 
       
   543     /**
       
   544      * Computes the changes of an association.
       
   545      *
       
   546      * @param AssociationMapping $assoc
       
   547      * @param mixed $value The value of the association.
       
   548      */
       
   549     private function computeAssociationChanges($assoc, $value)
       
   550     {
       
   551         if ($value instanceof PersistentCollection && $value->isDirty()) {
       
   552             if ($assoc['isOwningSide']) {
       
   553                 $this->collectionUpdates[] = $value;
       
   554             }
       
   555             $this->visitedCollections[] = $value;
       
   556         }
       
   557 
       
   558         // Look through the entities, and in any of their associations, for transient (new)
       
   559         // enities, recursively. ("Persistence by reachability")
       
   560         if ($assoc['type'] & ClassMetadata::TO_ONE) {
       
   561             if ($value instanceof Proxy && ! $value->__isInitialized__) {
       
   562                 return; // Ignore uninitialized proxy objects
       
   563             }
       
   564             $value = array($value);
       
   565         } else if ($value instanceof PersistentCollection) {
       
   566             // Unwrap. Uninitialized collections will simply be empty.
       
   567             $value = $value->unwrap();
       
   568         }
       
   569 
       
   570         $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
       
   571         foreach ($value as $entry) {
       
   572             $state = $this->getEntityState($entry, self::STATE_NEW);
       
   573             $oid = spl_object_hash($entry);
       
   574             if ($state == self::STATE_NEW) {
       
   575                 if ( ! $assoc['isCascadePersist']) {
       
   576                     throw new InvalidArgumentException("A new entity was found through the relationship '"
       
   577                             . $assoc['sourceEntity'] . "#" . $assoc['fieldName'] . "' that was not"
       
   578                             . " configured to cascade persist operations for entity: " . self::objToStr($entry) . "."
       
   579                             . " Explicitly persist the new entity or configure cascading persist operations"
       
   580                             . " on the relationship. If you cannot find out which entity causes the problem"
       
   581                             . " implement '" . $assoc['targetEntity'] . "#__toString()' to get a clue.");
       
   582                 }
       
   583                 $this->persistNew($targetClass, $entry);
       
   584                 $this->computeChangeSet($targetClass, $entry);
       
   585             } else if ($state == self::STATE_REMOVED) {
       
   586                 return new InvalidArgumentException("Removed entity detected during flush: "
       
   587                         . self::objToStr($entry).". Remove deleted entities from associations.");
       
   588             } else if ($state == self::STATE_DETACHED) {
       
   589                 // Can actually not happen right now as we assume STATE_NEW,
       
   590                 // so the exception will be raised from the DBAL layer (constraint violation).
       
   591                 throw new InvalidArgumentException("A detached entity was found through a "
       
   592                         . "relationship during cascading a persist operation.");
       
   593             }
       
   594             // MANAGED associated entities are already taken into account
       
   595             // during changeset calculation anyway, since they are in the identity map.
       
   596         }
       
   597     }
       
   598 
       
   599     private function persistNew($class, $entity)
       
   600     {
       
   601         $oid = spl_object_hash($entity);
       
   602         if (isset($class->lifecycleCallbacks[Events::prePersist])) {
       
   603             $class->invokeLifecycleCallbacks(Events::prePersist, $entity);
       
   604         }
       
   605         if ($this->evm->hasListeners(Events::prePersist)) {
       
   606             $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->em));
       
   607         }
       
   608 
       
   609         $idGen = $class->idGenerator;
       
   610         if ( ! $idGen->isPostInsertGenerator()) {
       
   611             $idValue = $idGen->generate($this->em, $entity);
       
   612             if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
       
   613                 $this->entityIdentifiers[$oid] = array($class->identifier[0] => $idValue);
       
   614                 $class->setIdentifierValues($entity, $this->entityIdentifiers[$oid]);
       
   615             } else {
       
   616                 $this->entityIdentifiers[$oid] = $idValue;
       
   617             }
       
   618         }
       
   619         $this->entityStates[$oid] = self::STATE_MANAGED;
       
   620 
       
   621         $this->scheduleForInsert($entity);
       
   622     }
       
   623     
       
   624     /**
       
   625      * INTERNAL:
       
   626      * Computes the changeset of an individual entity, independently of the
       
   627      * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
       
   628      * 
       
   629      * The passed entity must be a managed entity. If the entity already has a change set
       
   630      * because this method is invoked during a commit cycle then the change sets are added.
       
   631      * whereby changes detected in this method prevail.
       
   632      * 
       
   633      * @ignore
       
   634      * @param ClassMetadata $class The class descriptor of the entity.
       
   635      * @param object $entity The entity for which to (re)calculate the change set.
       
   636      * @throws InvalidArgumentException If the passed entity is not MANAGED.
       
   637      */
       
   638     public function recomputeSingleEntityChangeSet($class, $entity)
       
   639     {
       
   640         $oid = spl_object_hash($entity);
       
   641         
       
   642         if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
       
   643             throw new InvalidArgumentException('Entity must be managed.');
       
   644         }
       
   645         
       
   646         /* TODO: Just return if changetracking policy is NOTIFY?
       
   647         if ($class->isChangeTrackingNotify()) {
       
   648             return;
       
   649         }*/
       
   650 
       
   651         if ( ! $class->isInheritanceTypeNone()) {
       
   652             $class = $this->em->getClassMetadata(get_class($entity));
       
   653         }
       
   654 
       
   655         $actualData = array();
       
   656         foreach ($class->reflFields as $name => $refProp) {
       
   657             if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
       
   658                 $actualData[$name] = $refProp->getValue($entity);
       
   659             }
       
   660         }
       
   661 
       
   662         $originalData = $this->originalEntityData[$oid];
       
   663         $changeSet = array();
       
   664 
       
   665         foreach ($actualData as $propName => $actualValue) {
       
   666             $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
       
   667             if (is_object($orgValue) && $orgValue !== $actualValue) {
       
   668                 $changeSet[$propName] = array($orgValue, $actualValue);
       
   669             } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
       
   670                 $changeSet[$propName] = array($orgValue, $actualValue);
       
   671             }
       
   672         }
       
   673 
       
   674         if ($changeSet) {
       
   675             if (isset($this->entityChangeSets[$oid])) {
       
   676                 $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
       
   677             }
       
   678             $this->originalEntityData[$oid] = $actualData;
       
   679         }
       
   680     }
       
   681 
       
   682     /**
       
   683      * Executes all entity insertions for entities of the specified type.
       
   684      *
       
   685      * @param Doctrine\ORM\Mapping\ClassMetadata $class
       
   686      */
       
   687     private function executeInserts($class)
       
   688     {
       
   689         $className = $class->name;
       
   690         $persister = $this->getEntityPersister($className);
       
   691         
       
   692         $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
       
   693         $hasListeners = $this->evm->hasListeners(Events::postPersist);
       
   694         if ($hasLifecycleCallbacks || $hasListeners) {
       
   695             $entities = array();
       
   696         }
       
   697         
       
   698         foreach ($this->entityInsertions as $oid => $entity) {
       
   699             if (get_class($entity) === $className) {
       
   700                 $persister->addInsert($entity);
       
   701                 unset($this->entityInsertions[$oid]);
       
   702                 if ($hasLifecycleCallbacks || $hasListeners) {
       
   703                     $entities[] = $entity;
       
   704                 }
       
   705             }
       
   706         }
       
   707 
       
   708         $postInsertIds = $persister->executeInserts();
       
   709 
       
   710         if ($postInsertIds) {
       
   711             // Persister returned post-insert IDs
       
   712             foreach ($postInsertIds as $id => $entity) {
       
   713                 $oid = spl_object_hash($entity);
       
   714                 $idField = $class->identifier[0];
       
   715                 $class->reflFields[$idField]->setValue($entity, $id);
       
   716                 $this->entityIdentifiers[$oid] = array($idField => $id);
       
   717                 $this->entityStates[$oid] = self::STATE_MANAGED;
       
   718                 $this->originalEntityData[$oid][$idField] = $id;
       
   719                 $this->addToIdentityMap($entity);
       
   720             }
       
   721         }
       
   722         
       
   723         if ($hasLifecycleCallbacks || $hasListeners) {
       
   724             foreach ($entities as $entity) {
       
   725                 if ($hasLifecycleCallbacks) {
       
   726                     $class->invokeLifecycleCallbacks(Events::postPersist, $entity);
       
   727                 }
       
   728                 if ($hasListeners) {
       
   729                     $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em));
       
   730                 }
       
   731             }
       
   732         }
       
   733     }
       
   734 
       
   735     /**
       
   736      * Executes all entity updates for entities of the specified type.
       
   737      *
       
   738      * @param Doctrine\ORM\Mapping\ClassMetadata $class
       
   739      */
       
   740     private function executeUpdates($class)
       
   741     {
       
   742         $className = $class->name;
       
   743         $persister = $this->getEntityPersister($className);
       
   744 
       
   745         $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]);
       
   746         $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
       
   747         $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]);
       
   748         $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
       
   749         
       
   750         foreach ($this->entityUpdates as $oid => $entity) {
       
   751             if (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className) {
       
   752                 
       
   753                 if ($hasPreUpdateLifecycleCallbacks) {
       
   754                     $class->invokeLifecycleCallbacks(Events::preUpdate, $entity);
       
   755                     $this->recomputeSingleEntityChangeSet($class, $entity);
       
   756                 }
       
   757                 
       
   758                 if ($hasPreUpdateListeners) {
       
   759                     $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
       
   760                         $entity, $this->em, $this->entityChangeSets[$oid])
       
   761                     );
       
   762                 }
       
   763 
       
   764                 if ($this->entityChangeSets[$oid]) {
       
   765                     $persister->update($entity);
       
   766                 }
       
   767                 unset($this->entityUpdates[$oid]);
       
   768                 
       
   769                 if ($hasPostUpdateLifecycleCallbacks) {
       
   770                     $class->invokeLifecycleCallbacks(Events::postUpdate, $entity);
       
   771                 }
       
   772                 if ($hasPostUpdateListeners) {
       
   773                     $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em));
       
   774                 }
       
   775             }
       
   776         }
       
   777     }
       
   778 
       
   779     /**
       
   780      * Executes all entity deletions for entities of the specified type.
       
   781      *
       
   782      * @param Doctrine\ORM\Mapping\ClassMetadata $class
       
   783      */
       
   784     private function executeDeletions($class)
       
   785     {
       
   786         $className = $class->name;
       
   787         $persister = $this->getEntityPersister($className);
       
   788                 
       
   789         $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]);
       
   790         $hasListeners = $this->evm->hasListeners(Events::postRemove);
       
   791         
       
   792         foreach ($this->entityDeletions as $oid => $entity) {
       
   793             if (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className) {
       
   794                 $persister->delete($entity);
       
   795                 unset(
       
   796                     $this->entityDeletions[$oid],
       
   797                     $this->entityIdentifiers[$oid],
       
   798                     $this->originalEntityData[$oid],
       
   799                     $this->entityStates[$oid]
       
   800                     );
       
   801                 // Entity with this $oid after deletion treated as NEW, even if the $oid
       
   802                 // is obtained by a new entity because the old one went out of scope.
       
   803                 //$this->entityStates[$oid] = self::STATE_NEW;
       
   804                 if ( ! $class->isIdentifierNatural()) {
       
   805                     $class->reflFields[$class->identifier[0]]->setValue($entity, null);
       
   806                 }
       
   807 
       
   808                 if ($hasLifecycleCallbacks) {
       
   809                     $class->invokeLifecycleCallbacks(Events::postRemove, $entity);
       
   810                 }
       
   811                 if ($hasListeners) {
       
   812                     $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em));
       
   813                 }
       
   814             }
       
   815         }
       
   816     }
       
   817 
       
   818     /**
       
   819      * Gets the commit order.
       
   820      *
       
   821      * @return array
       
   822      */
       
   823     private function getCommitOrder(array $entityChangeSet = null)
       
   824     {
       
   825         if ($entityChangeSet === null) {
       
   826             $entityChangeSet = array_merge(
       
   827                     $this->entityInsertions,
       
   828                     $this->entityUpdates,
       
   829                     $this->entityDeletions
       
   830                     );
       
   831         }
       
   832         
       
   833         $calc = $this->getCommitOrderCalculator();
       
   834         
       
   835         // See if there are any new classes in the changeset, that are not in the
       
   836         // commit order graph yet (dont have a node).
       
   837         $newNodes = array();
       
   838         foreach ($entityChangeSet as $oid => $entity) {
       
   839             $className = get_class($entity);         
       
   840             if ( ! $calc->hasClass($className)) {
       
   841                 $class = $this->em->getClassMetadata($className);
       
   842                 $calc->addClass($class);
       
   843                 $newNodes[] = $class;
       
   844             }
       
   845         }
       
   846 
       
   847         // Calculate dependencies for new nodes
       
   848         foreach ($newNodes as $class) {
       
   849             foreach ($class->associationMappings as $assoc) {
       
   850                 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
       
   851                     $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
       
   852                     if ( ! $calc->hasClass($targetClass->name)) {
       
   853                         $calc->addClass($targetClass);
       
   854                     }
       
   855                     $calc->addDependency($targetClass, $class);
       
   856                     // If the target class has mapped subclasses,
       
   857                     // these share the same dependency.
       
   858                     if ($targetClass->subClasses) {
       
   859                         foreach ($targetClass->subClasses as $subClassName) {
       
   860                             $targetSubClass = $this->em->getClassMetadata($subClassName);
       
   861                             if ( ! $calc->hasClass($subClassName)) {
       
   862                                 $calc->addClass($targetSubClass);
       
   863                             }
       
   864                             $calc->addDependency($targetSubClass, $class);
       
   865                         }
       
   866                     }
       
   867                 }
       
   868             }
       
   869         }
       
   870 
       
   871         return $calc->getCommitOrder();
       
   872     }
       
   873 
       
   874     /**
       
   875      * Schedules an entity for insertion into the database.
       
   876      * If the entity already has an identifier, it will be added to the identity map.
       
   877      *
       
   878      * @param object $entity The entity to schedule for insertion.
       
   879      */
       
   880     public function scheduleForInsert($entity)
       
   881     {
       
   882         $oid = spl_object_hash($entity);
       
   883 
       
   884         if (isset($this->entityUpdates[$oid])) {
       
   885             throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
       
   886         }
       
   887         if (isset($this->entityDeletions[$oid])) {
       
   888             throw new InvalidArgumentException("Removed entity can not be scheduled for insertion.");
       
   889         }
       
   890         if (isset($this->entityInsertions[$oid])) {
       
   891             throw new InvalidArgumentException("Entity can not be scheduled for insertion twice.");
       
   892         }
       
   893 
       
   894         $this->entityInsertions[$oid] = $entity;
       
   895 
       
   896         if (isset($this->entityIdentifiers[$oid])) {
       
   897             $this->addToIdentityMap($entity);
       
   898         }
       
   899     }
       
   900 
       
   901     /**
       
   902      * Checks whether an entity is scheduled for insertion.
       
   903      *
       
   904      * @param object $entity
       
   905      * @return boolean
       
   906      */
       
   907     public function isScheduledForInsert($entity)
       
   908     {
       
   909         return isset($this->entityInsertions[spl_object_hash($entity)]);
       
   910     }
       
   911 
       
   912     /**
       
   913      * Schedules an entity for being updated.
       
   914      *
       
   915      * @param object $entity The entity to schedule for being updated.
       
   916      */
       
   917     public function scheduleForUpdate($entity)
       
   918     {
       
   919         $oid = spl_object_hash($entity);
       
   920         if ( ! isset($this->entityIdentifiers[$oid])) {
       
   921             throw new InvalidArgumentException("Entity has no identity.");
       
   922         }
       
   923         if (isset($this->entityDeletions[$oid])) {
       
   924             throw new InvalidArgumentException("Entity is removed.");
       
   925         }
       
   926 
       
   927         if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
       
   928             $this->entityUpdates[$oid] = $entity;
       
   929         }
       
   930     }
       
   931     
       
   932     /**
       
   933      * INTERNAL:
       
   934      * Schedules an extra update that will be executed immediately after the
       
   935      * regular entity updates within the currently running commit cycle.
       
   936      * 
       
   937      * Extra updates for entities are stored as (entity, changeset) tuples.
       
   938      * 
       
   939      * @ignore
       
   940      * @param object $entity The entity for which to schedule an extra update.
       
   941      * @param array $changeset The changeset of the entity (what to update).
       
   942      */
       
   943     public function scheduleExtraUpdate($entity, array $changeset)
       
   944     {
       
   945         $oid = spl_object_hash($entity);
       
   946         if (isset($this->extraUpdates[$oid])) {
       
   947             list($ignored, $changeset2) = $this->extraUpdates[$oid];
       
   948             $this->extraUpdates[$oid] = array($entity, $changeset + $changeset2);
       
   949         } else {
       
   950             $this->extraUpdates[$oid] = array($entity, $changeset);
       
   951         }
       
   952     }
       
   953 
       
   954     /**
       
   955      * Checks whether an entity is registered as dirty in the unit of work.
       
   956      * Note: Is not very useful currently as dirty entities are only registered
       
   957      * at commit time.
       
   958      *
       
   959      * @param object $entity
       
   960      * @return boolean
       
   961      */
       
   962     public function isScheduledForUpdate($entity)
       
   963     {
       
   964         return isset($this->entityUpdates[spl_object_hash($entity)]);
       
   965     }
       
   966 
       
   967     public function isScheduledForDirtyCheck($entity)
       
   968     {
       
   969         $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
       
   970         return isset($this->scheduledForDirtyCheck[$rootEntityName][spl_object_hash($entity)]);
       
   971     }
       
   972 
       
   973     /**
       
   974      * INTERNAL:
       
   975      * Schedules an entity for deletion.
       
   976      * 
       
   977      * @param object $entity
       
   978      */
       
   979     public function scheduleForDelete($entity)
       
   980     {
       
   981         $oid = spl_object_hash($entity);
       
   982         
       
   983         if (isset($this->entityInsertions[$oid])) {
       
   984             if ($this->isInIdentityMap($entity)) {
       
   985                 $this->removeFromIdentityMap($entity);
       
   986             }
       
   987             unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
       
   988             return; // entity has not been persisted yet, so nothing more to do.
       
   989         }
       
   990 
       
   991         if ( ! $this->isInIdentityMap($entity)) {
       
   992             return; // ignore
       
   993         }
       
   994 
       
   995         $this->removeFromIdentityMap($entity);
       
   996 
       
   997         if (isset($this->entityUpdates[$oid])) {
       
   998             unset($this->entityUpdates[$oid]);
       
   999         }
       
  1000         if ( ! isset($this->entityDeletions[$oid])) {
       
  1001             $this->entityDeletions[$oid] = $entity;
       
  1002             $this->entityStates[$oid] = self::STATE_REMOVED;
       
  1003         }
       
  1004     }
       
  1005 
       
  1006     /**
       
  1007      * Checks whether an entity is registered as removed/deleted with the unit
       
  1008      * of work.
       
  1009      *
       
  1010      * @param object $entity
       
  1011      * @return boolean
       
  1012      */
       
  1013     public function isScheduledForDelete($entity)
       
  1014     {
       
  1015         return isset($this->entityDeletions[spl_object_hash($entity)]);
       
  1016     }
       
  1017 
       
  1018     /**
       
  1019      * Checks whether an entity is scheduled for insertion, update or deletion.
       
  1020      * 
       
  1021      * @param $entity
       
  1022      * @return boolean
       
  1023      */
       
  1024     public function isEntityScheduled($entity)
       
  1025     {
       
  1026         $oid = spl_object_hash($entity);
       
  1027         return isset($this->entityInsertions[$oid]) ||
       
  1028                 isset($this->entityUpdates[$oid]) ||
       
  1029                 isset($this->entityDeletions[$oid]);
       
  1030     }
       
  1031 
       
  1032     /**
       
  1033      * INTERNAL:
       
  1034      * Registers an entity in the identity map.
       
  1035      * Note that entities in a hierarchy are registered with the class name of
       
  1036      * the root entity.
       
  1037      *
       
  1038      * @ignore
       
  1039      * @param object $entity  The entity to register.
       
  1040      * @return boolean  TRUE if the registration was successful, FALSE if the identity of
       
  1041      *                  the entity in question is already managed.
       
  1042      */
       
  1043     public function addToIdentityMap($entity)
       
  1044     {
       
  1045         $classMetadata = $this->em->getClassMetadata(get_class($entity));
       
  1046         $idHash = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
       
  1047         if ($idHash === '') {
       
  1048             throw new InvalidArgumentException("The given entity has no identity.");
       
  1049         }
       
  1050         $className = $classMetadata->rootEntityName;
       
  1051         if (isset($this->identityMap[$className][$idHash])) {
       
  1052             return false;
       
  1053         }
       
  1054         $this->identityMap[$className][$idHash] = $entity;
       
  1055         if ($entity instanceof NotifyPropertyChanged) {
       
  1056             $entity->addPropertyChangedListener($this);
       
  1057         }
       
  1058         return true;
       
  1059     }
       
  1060 
       
  1061     /**
       
  1062      * Gets the state of an entity with regard to the current unit of work.
       
  1063      *
       
  1064      * @param object $entity
       
  1065      * @param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
       
  1066      *                        This parameter can be set to improve performance of entity state detection
       
  1067      *                        by potentially avoiding a database lookup if the distinction between NEW and DETACHED
       
  1068      *                        is either known or does not matter for the caller of the method.
       
  1069      * @return int The entity state.
       
  1070      */
       
  1071     public function getEntityState($entity, $assume = null)
       
  1072     {
       
  1073         $oid = spl_object_hash($entity);
       
  1074         if ( ! isset($this->entityStates[$oid])) {
       
  1075             // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
       
  1076             // Note that you can not remember the NEW or DETACHED state in _entityStates since
       
  1077             // the UoW does not hold references to such objects and the object hash can be reused.
       
  1078             // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
       
  1079             if ($assume === null) {
       
  1080                 $class = $this->em->getClassMetadata(get_class($entity));
       
  1081                 $id = $class->getIdentifierValues($entity);
       
  1082                 if ( ! $id) {
       
  1083                     return self::STATE_NEW;
       
  1084                 } else if ($class->isIdentifierNatural()) {
       
  1085                     // Check for a version field, if available, to avoid a db lookup.
       
  1086                     if ($class->isVersioned) {
       
  1087                         if ($class->getFieldValue($entity, $class->versionField)) {
       
  1088                             return self::STATE_DETACHED;
       
  1089                         } else {
       
  1090                             return self::STATE_NEW;
       
  1091                         }
       
  1092                     } else {
       
  1093                         // Last try before db lookup: check the identity map.
       
  1094                         if ($this->tryGetById($id, $class->rootEntityName)) {
       
  1095                             return self::STATE_DETACHED;
       
  1096                         } else {
       
  1097                             // db lookup
       
  1098                             if ($this->getEntityPersister(get_class($entity))->exists($entity)) {
       
  1099                                 return self::STATE_DETACHED;
       
  1100                             } else {
       
  1101                                 return self::STATE_NEW;
       
  1102                             }
       
  1103                         }
       
  1104                     }
       
  1105                 } else {
       
  1106                     return self::STATE_DETACHED;
       
  1107                 }
       
  1108             } else {
       
  1109                 return $assume;
       
  1110             }
       
  1111         }
       
  1112         return $this->entityStates[$oid];
       
  1113     }
       
  1114 
       
  1115     /**
       
  1116      * INTERNAL:
       
  1117      * Removes an entity from the identity map. This effectively detaches the
       
  1118      * entity from the persistence management of Doctrine.
       
  1119      *
       
  1120      * @ignore
       
  1121      * @param object $entity
       
  1122      * @return boolean
       
  1123      */
       
  1124     public function removeFromIdentityMap($entity)
       
  1125     {
       
  1126         $oid = spl_object_hash($entity);
       
  1127         $classMetadata = $this->em->getClassMetadata(get_class($entity));
       
  1128         $idHash = implode(' ', $this->entityIdentifiers[$oid]);
       
  1129         if ($idHash === '') {
       
  1130             throw new InvalidArgumentException("The given entity has no identity.");
       
  1131         }
       
  1132         $className = $classMetadata->rootEntityName;
       
  1133         if (isset($this->identityMap[$className][$idHash])) {
       
  1134             unset($this->identityMap[$className][$idHash]);
       
  1135             //$this->entityStates[$oid] = self::STATE_DETACHED;
       
  1136             return true;
       
  1137         }
       
  1138 
       
  1139         return false;
       
  1140     }
       
  1141 
       
  1142     /**
       
  1143      * INTERNAL:
       
  1144      * Gets an entity in the identity map by its identifier hash.
       
  1145      *
       
  1146      * @ignore
       
  1147      * @param string $idHash
       
  1148      * @param string $rootClassName
       
  1149      * @return object
       
  1150      */
       
  1151     public function getByIdHash($idHash, $rootClassName)
       
  1152     {
       
  1153         return $this->identityMap[$rootClassName][$idHash];
       
  1154     }
       
  1155 
       
  1156     /**
       
  1157      * INTERNAL:
       
  1158      * Tries to get an entity by its identifier hash. If no entity is found for
       
  1159      * the given hash, FALSE is returned.
       
  1160      *
       
  1161      * @ignore
       
  1162      * @param string $idHash
       
  1163      * @param string $rootClassName
       
  1164      * @return mixed The found entity or FALSE.
       
  1165      */
       
  1166     public function tryGetByIdHash($idHash, $rootClassName)
       
  1167     {
       
  1168         return isset($this->identityMap[$rootClassName][$idHash]) ?
       
  1169                 $this->identityMap[$rootClassName][$idHash] : false;
       
  1170     }
       
  1171 
       
  1172     /**
       
  1173      * Checks whether an entity is registered in the identity map of this UnitOfWork.
       
  1174      *
       
  1175      * @param object $entity
       
  1176      * @return boolean
       
  1177      */
       
  1178     public function isInIdentityMap($entity)
       
  1179     {
       
  1180         $oid = spl_object_hash($entity);
       
  1181         if ( ! isset($this->entityIdentifiers[$oid])) {
       
  1182             return false;
       
  1183         }
       
  1184         $classMetadata = $this->em->getClassMetadata(get_class($entity));
       
  1185         $idHash = implode(' ', $this->entityIdentifiers[$oid]);
       
  1186         if ($idHash === '') {
       
  1187             return false;
       
  1188         }
       
  1189         
       
  1190         return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
       
  1191     }
       
  1192 
       
  1193     /**
       
  1194      * INTERNAL:
       
  1195      * Checks whether an identifier hash exists in the identity map.
       
  1196      *
       
  1197      * @ignore
       
  1198      * @param string $idHash
       
  1199      * @param string $rootClassName
       
  1200      * @return boolean
       
  1201      */
       
  1202     public function containsIdHash($idHash, $rootClassName)
       
  1203     {
       
  1204         return isset($this->identityMap[$rootClassName][$idHash]);
       
  1205     }
       
  1206 
       
  1207     /**
       
  1208      * Persists an entity as part of the current unit of work.
       
  1209      *
       
  1210      * @param object $entity The entity to persist.
       
  1211      */
       
  1212     public function persist($entity)
       
  1213     {
       
  1214         $visited = array();
       
  1215         $this->doPersist($entity, $visited);
       
  1216     }
       
  1217 
       
  1218     /**
       
  1219      * Persists an entity as part of the current unit of work.
       
  1220      * 
       
  1221      * This method is internally called during persist() cascades as it tracks
       
  1222      * the already visited entities to prevent infinite recursions.
       
  1223      *
       
  1224      * @param object $entity The entity to persist.
       
  1225      * @param array $visited The already visited entities.
       
  1226      */
       
  1227     private function doPersist($entity, array &$visited)
       
  1228     {
       
  1229         $oid = spl_object_hash($entity);
       
  1230         if (isset($visited[$oid])) {
       
  1231             return; // Prevent infinite recursion
       
  1232         }
       
  1233 
       
  1234         $visited[$oid] = $entity; // Mark visited
       
  1235 
       
  1236         $class = $this->em->getClassMetadata(get_class($entity));
       
  1237 
       
  1238         // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
       
  1239         // If we would detect DETACHED here we would throw an exception anyway with the same
       
  1240         // consequences (not recoverable/programming error), so just assuming NEW here
       
  1241         // lets us avoid some database lookups for entities with natural identifiers.
       
  1242         $entityState = $this->getEntityState($entity, self::STATE_NEW);
       
  1243 
       
  1244         switch ($entityState) {
       
  1245             case self::STATE_MANAGED:
       
  1246                 // Nothing to do, except if policy is "deferred explicit"
       
  1247                 if ($class->isChangeTrackingDeferredExplicit()) {
       
  1248                     $this->scheduleForDirtyCheck($entity);
       
  1249                 }
       
  1250                 break;
       
  1251             case self::STATE_NEW:
       
  1252                 $this->persistNew($class, $entity);
       
  1253                 break;
       
  1254             case self::STATE_REMOVED:
       
  1255                 // Entity becomes managed again
       
  1256                 unset($this->entityDeletions[$oid]);
       
  1257                 $this->entityStates[$oid] = self::STATE_MANAGED;
       
  1258                 break;
       
  1259             case self::STATE_DETACHED:
       
  1260                 // Can actually not happen right now since we assume STATE_NEW.
       
  1261                 throw new InvalidArgumentException("Detached entity passed to persist().");
       
  1262             default:
       
  1263                 throw new UnexpectedValueException("Unexpected entity state: $entityState.");
       
  1264         }
       
  1265 
       
  1266         $this->cascadePersist($entity, $visited);
       
  1267     }
       
  1268 
       
  1269     /**
       
  1270      * Deletes an entity as part of the current unit of work.
       
  1271      *
       
  1272      * @param object $entity The entity to remove.
       
  1273      */
       
  1274     public function remove($entity)
       
  1275     {
       
  1276         $visited = array();
       
  1277         $this->doRemove($entity, $visited);
       
  1278     }
       
  1279 
       
  1280     /**
       
  1281      * Deletes an entity as part of the current unit of work.
       
  1282      *
       
  1283      * This method is internally called during delete() cascades as it tracks
       
  1284      * the already visited entities to prevent infinite recursions.
       
  1285      *
       
  1286      * @param object $entity The entity to delete.
       
  1287      * @param array $visited The map of the already visited entities.
       
  1288      * @throws InvalidArgumentException If the instance is a detached entity.
       
  1289      */
       
  1290     private function doRemove($entity, array &$visited)
       
  1291     {
       
  1292         $oid = spl_object_hash($entity);
       
  1293         if (isset($visited[$oid])) {
       
  1294             return; // Prevent infinite recursion
       
  1295         }
       
  1296 
       
  1297         $visited[$oid] = $entity; // mark visited
       
  1298         
       
  1299         // Cascade first, because scheduleForDelete() removes the entity from the identity map, which 
       
  1300         // can cause problems when a lazy proxy has to be initialized for the cascade operation.
       
  1301         $this->cascadeRemove($entity, $visited);
       
  1302 
       
  1303         $class = $this->em->getClassMetadata(get_class($entity));
       
  1304         $entityState = $this->getEntityState($entity);
       
  1305         switch ($entityState) {
       
  1306             case self::STATE_NEW:
       
  1307             case self::STATE_REMOVED:
       
  1308                 // nothing to do
       
  1309                 break;
       
  1310             case self::STATE_MANAGED:
       
  1311                 if (isset($class->lifecycleCallbacks[Events::preRemove])) {
       
  1312                     $class->invokeLifecycleCallbacks(Events::preRemove, $entity);
       
  1313                 }
       
  1314                 if ($this->evm->hasListeners(Events::preRemove)) {
       
  1315                     $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity, $this->em));
       
  1316                 }
       
  1317                 $this->scheduleForDelete($entity);
       
  1318                 break;
       
  1319             case self::STATE_DETACHED:
       
  1320                 throw new InvalidArgumentException("A detached entity can not be removed.");
       
  1321             default:
       
  1322                 throw new UnexpectedValueException("Unexpected entity state: $entityState.");
       
  1323         }
       
  1324 
       
  1325     }
       
  1326 
       
  1327     /**
       
  1328      * Merges the state of the given detached entity into this UnitOfWork.
       
  1329      *
       
  1330      * @param object $entity
       
  1331      * @return object The managed copy of the entity.
       
  1332      * @throws OptimisticLockException If the entity uses optimistic locking through a version
       
  1333      *         attribute and the version check against the managed copy fails.
       
  1334      *
       
  1335      * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
       
  1336      */
       
  1337     public function merge($entity)
       
  1338     {
       
  1339         $visited = array();
       
  1340         return $this->doMerge($entity, $visited);
       
  1341     }
       
  1342 
       
  1343     /**
       
  1344      * Executes a merge operation on an entity.
       
  1345      *
       
  1346      * @param object $entity
       
  1347      * @param array $visited
       
  1348      * @return object The managed copy of the entity.
       
  1349      * @throws OptimisticLockException If the entity uses optimistic locking through a version
       
  1350      *         attribute and the version check against the managed copy fails.
       
  1351      * @throws InvalidArgumentException If the entity instance is NEW.
       
  1352      */
       
  1353     private function doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
       
  1354     {
       
  1355         $oid = spl_object_hash($entity);
       
  1356         if (isset($visited[$oid])) {
       
  1357             return; // Prevent infinite recursion
       
  1358         }
       
  1359 
       
  1360         $visited[$oid] = $entity; // mark visited
       
  1361 
       
  1362         $class = $this->em->getClassMetadata(get_class($entity));
       
  1363 
       
  1364         // First we assume DETACHED, although it can still be NEW but we can avoid
       
  1365         // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
       
  1366         // we need to fetch it from the db anyway in order to merge.
       
  1367         // MANAGED entities are ignored by the merge operation.
       
  1368         if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) {
       
  1369             $managedCopy = $entity;
       
  1370         } else {
       
  1371             // Try to look the entity up in the identity map.
       
  1372             $id = $class->getIdentifierValues($entity);
       
  1373 
       
  1374             // If there is no ID, it is actually NEW.
       
  1375             if ( ! $id) {
       
  1376                 $managedCopy = $class->newInstance();
       
  1377                 $this->persistNew($class, $managedCopy);
       
  1378             } else {
       
  1379                 $managedCopy = $this->tryGetById($id, $class->rootEntityName);
       
  1380                 if ($managedCopy) {
       
  1381                     // We have the entity in-memory already, just make sure its not removed.
       
  1382                     if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
       
  1383                         throw new InvalidArgumentException('Removed entity detected during merge.'
       
  1384                                 . ' Can not merge with a removed entity.');
       
  1385                     }
       
  1386                 } else {
       
  1387                     // We need to fetch the managed copy in order to merge.
       
  1388                     $managedCopy = $this->em->find($class->name, $id);
       
  1389                 }
       
  1390 
       
  1391                 if ($managedCopy === null) {
       
  1392                     // If the identifier is ASSIGNED, it is NEW, otherwise an error
       
  1393                     // since the managed entity was not found.
       
  1394                     if ($class->isIdentifierNatural()) {
       
  1395                         $managedCopy = $class->newInstance();
       
  1396                         $class->setIdentifierValues($managedCopy, $id);
       
  1397                         $this->persistNew($class, $managedCopy);
       
  1398                     } else {
       
  1399                         throw new EntityNotFoundException;
       
  1400                     }
       
  1401                 }
       
  1402             }
       
  1403 
       
  1404             if ($class->isVersioned) {
       
  1405                 $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
       
  1406                 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
       
  1407                 // Throw exception if versions dont match.
       
  1408                 if ($managedCopyVersion != $entityVersion) {
       
  1409                     throw OptimisticLockException::lockFailedVersionMissmatch($entity, $entityVersion, $managedCopyVersion);
       
  1410                 }
       
  1411             }
       
  1412 
       
  1413             // Merge state of $entity into existing (managed) entity
       
  1414             foreach ($class->reflFields as $name => $prop) {
       
  1415                 if ( ! isset($class->associationMappings[$name])) {
       
  1416                     if ( ! $class->isIdentifier($name)) {
       
  1417                         $prop->setValue($managedCopy, $prop->getValue($entity));
       
  1418                     }
       
  1419                 } else {
       
  1420                     $assoc2 = $class->associationMappings[$name];
       
  1421                     if ($assoc2['type'] & ClassMetadata::TO_ONE) {
       
  1422                         $other = $prop->getValue($entity);
       
  1423                         if ($other === null) {
       
  1424                             $prop->setValue($managedCopy, null);
       
  1425                         } else if ($other instanceof Proxy && !$other->__isInitialized__) {
       
  1426                             // do not merge fields marked lazy that have not been fetched.
       
  1427                             continue;
       
  1428                         } else if ( ! $assoc2['isCascadeMerge']) {
       
  1429                             if ($this->getEntityState($other, self::STATE_DETACHED) == self::STATE_MANAGED) {
       
  1430                                 $prop->setValue($managedCopy, $other);
       
  1431                             } else {
       
  1432                                 $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
       
  1433                                 $id = $targetClass->getIdentifierValues($other);
       
  1434                                 $proxy = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $id);
       
  1435                                 $prop->setValue($managedCopy, $proxy);
       
  1436                                 $this->registerManaged($proxy, $id, array());
       
  1437                             }
       
  1438                         }
       
  1439                     } else {
       
  1440                         $mergeCol = $prop->getValue($entity);
       
  1441                         if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
       
  1442                             // do not merge fields marked lazy that have not been fetched.
       
  1443                             // keep the lazy persistent collection of the managed copy.
       
  1444                             continue;
       
  1445                         }
       
  1446 
       
  1447                         $managedCol = $prop->getValue($managedCopy);
       
  1448                         if (!$managedCol) {
       
  1449                             $managedCol = new PersistentCollection($this->em,
       
  1450                                     $this->em->getClassMetadata($assoc2['targetEntity']),
       
  1451                                     new ArrayCollection
       
  1452                                     );
       
  1453                             $managedCol->setOwner($managedCopy, $assoc2);
       
  1454                             $prop->setValue($managedCopy, $managedCol);
       
  1455                             $this->originalEntityData[$oid][$name] = $managedCol;
       
  1456                         }
       
  1457                         if ($assoc2['isCascadeMerge']) {
       
  1458                             $managedCol->initialize();
       
  1459                             // clear and set dirty a managed collection if its not also the same collection to merge from.
       
  1460                             if (!$managedCol->isEmpty() && $managedCol != $mergeCol) {
       
  1461                                 $managedCol->unwrap()->clear();
       
  1462                                 $managedCol->setDirty(true);
       
  1463                                 if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) {
       
  1464                                     $this->scheduleForDirtyCheck($managedCopy);
       
  1465                                 }
       
  1466                             }
       
  1467                         }
       
  1468                     }
       
  1469                 }
       
  1470                 if ($class->isChangeTrackingNotify()) {
       
  1471                     // Just treat all properties as changed, there is no other choice.
       
  1472                     $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
       
  1473                 }
       
  1474             }
       
  1475             if ($class->isChangeTrackingDeferredExplicit()) {
       
  1476                 $this->scheduleForDirtyCheck($entity);
       
  1477             }
       
  1478         }
       
  1479 
       
  1480         if ($prevManagedCopy !== null) {
       
  1481             $assocField = $assoc['fieldName'];
       
  1482             $prevClass = $this->em->getClassMetadata(get_class($prevManagedCopy));
       
  1483             if ($assoc['type'] & ClassMetadata::TO_ONE) {
       
  1484                 $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
       
  1485             } else {
       
  1486                 $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
       
  1487                 if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) {
       
  1488                     $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
       
  1489                 }
       
  1490             }
       
  1491         }
       
  1492 
       
  1493         // Mark the managed copy visited as well
       
  1494         $visited[spl_object_hash($managedCopy)] = true;
       
  1495 
       
  1496         $this->cascadeMerge($entity, $managedCopy, $visited);
       
  1497 
       
  1498         return $managedCopy;
       
  1499     }
       
  1500     
       
  1501     /**
       
  1502      * Detaches an entity from the persistence management. It's persistence will
       
  1503      * no longer be managed by Doctrine.
       
  1504      *
       
  1505      * @param object $entity The entity to detach.
       
  1506      */
       
  1507     public function detach($entity)
       
  1508     {
       
  1509         $visited = array();
       
  1510         $this->doDetach($entity, $visited);
       
  1511     }
       
  1512     
       
  1513     /**
       
  1514      * Executes a detach operation on the given entity.
       
  1515      * 
       
  1516      * @param object $entity
       
  1517      * @param array $visited
       
  1518      */
       
  1519     private function doDetach($entity, array &$visited)
       
  1520     {
       
  1521         $oid = spl_object_hash($entity);
       
  1522         if (isset($visited[$oid])) {
       
  1523             return; // Prevent infinite recursion
       
  1524         }
       
  1525 
       
  1526         $visited[$oid] = $entity; // mark visited
       
  1527         
       
  1528         switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
       
  1529             case self::STATE_MANAGED:
       
  1530                 if ($this->isInIdentityMap($entity)) {
       
  1531                     $this->removeFromIdentityMap($entity);
       
  1532                 }
       
  1533                 unset($this->entityInsertions[$oid], $this->entityUpdates[$oid],
       
  1534                         $this->entityDeletions[$oid], $this->entityIdentifiers[$oid],
       
  1535                         $this->entityStates[$oid], $this->originalEntityData[$oid]);
       
  1536                 break;
       
  1537             case self::STATE_NEW:
       
  1538             case self::STATE_DETACHED:
       
  1539                 return;
       
  1540         }
       
  1541         
       
  1542         $this->cascadeDetach($entity, $visited);
       
  1543     }
       
  1544     
       
  1545     /**
       
  1546      * Refreshes the state of the given entity from the database, overwriting
       
  1547      * any local, unpersisted changes.
       
  1548      * 
       
  1549      * @param object $entity The entity to refresh.
       
  1550      * @throws InvalidArgumentException If the entity is not MANAGED.
       
  1551      */
       
  1552     public function refresh($entity)
       
  1553     {
       
  1554         $visited = array();
       
  1555         $this->doRefresh($entity, $visited);
       
  1556     }
       
  1557     
       
  1558     /**
       
  1559      * Executes a refresh operation on an entity.
       
  1560      * 
       
  1561      * @param object $entity The entity to refresh.
       
  1562      * @param array $visited The already visited entities during cascades.
       
  1563      * @throws InvalidArgumentException If the entity is not MANAGED.
       
  1564      */
       
  1565     private function doRefresh($entity, array &$visited)
       
  1566     {
       
  1567         $oid = spl_object_hash($entity);
       
  1568         if (isset($visited[$oid])) {
       
  1569             return; // Prevent infinite recursion
       
  1570         }
       
  1571 
       
  1572         $visited[$oid] = $entity; // mark visited
       
  1573 
       
  1574         $class = $this->em->getClassMetadata(get_class($entity));
       
  1575         if ($this->getEntityState($entity) == self::STATE_MANAGED) {
       
  1576             $this->getEntityPersister($class->name)->refresh(
       
  1577                 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
       
  1578                 $entity
       
  1579             );
       
  1580         } else {
       
  1581             throw new InvalidArgumentException("Entity is not MANAGED.");
       
  1582         }
       
  1583         
       
  1584         $this->cascadeRefresh($entity, $visited);
       
  1585     }
       
  1586     
       
  1587     /**
       
  1588      * Cascades a refresh operation to associated entities.
       
  1589      *
       
  1590      * @param object $entity
       
  1591      * @param array $visited
       
  1592      */
       
  1593     private function cascadeRefresh($entity, array &$visited)
       
  1594     {
       
  1595         $class = $this->em->getClassMetadata(get_class($entity));
       
  1596         foreach ($class->associationMappings as $assoc) {
       
  1597             if ( ! $assoc['isCascadeRefresh']) {
       
  1598                 continue;
       
  1599             }
       
  1600             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
       
  1601             if ($relatedEntities instanceof Collection) {
       
  1602                 if ($relatedEntities instanceof PersistentCollection) {
       
  1603                     // Unwrap so that foreach() does not initialize
       
  1604                     $relatedEntities = $relatedEntities->unwrap();
       
  1605                 }
       
  1606                 foreach ($relatedEntities as $relatedEntity) {
       
  1607                     $this->doRefresh($relatedEntity, $visited);
       
  1608                 }
       
  1609             } else if ($relatedEntities !== null) {
       
  1610                 $this->doRefresh($relatedEntities, $visited);
       
  1611             }
       
  1612         }
       
  1613     }
       
  1614     
       
  1615     /**
       
  1616      * Cascades a detach operation to associated entities.
       
  1617      *
       
  1618      * @param object $entity
       
  1619      * @param array $visited
       
  1620      */
       
  1621     private function cascadeDetach($entity, array &$visited)
       
  1622     {
       
  1623         $class = $this->em->getClassMetadata(get_class($entity));
       
  1624         foreach ($class->associationMappings as $assoc) {
       
  1625             if ( ! $assoc['isCascadeDetach']) {
       
  1626                 continue;
       
  1627             }
       
  1628             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
       
  1629             if ($relatedEntities instanceof Collection) {
       
  1630                 if ($relatedEntities instanceof PersistentCollection) {
       
  1631                     // Unwrap so that foreach() does not initialize
       
  1632                     $relatedEntities = $relatedEntities->unwrap();
       
  1633                 }
       
  1634                 foreach ($relatedEntities as $relatedEntity) {
       
  1635                     $this->doDetach($relatedEntity, $visited);
       
  1636                 }
       
  1637             } else if ($relatedEntities !== null) {
       
  1638                 $this->doDetach($relatedEntities, $visited);
       
  1639             }
       
  1640         }
       
  1641     }
       
  1642 
       
  1643     /**
       
  1644      * Cascades a merge operation to associated entities.
       
  1645      *
       
  1646      * @param object $entity
       
  1647      * @param object $managedCopy
       
  1648      * @param array $visited
       
  1649      */
       
  1650     private function cascadeMerge($entity, $managedCopy, array &$visited)
       
  1651     {
       
  1652         $class = $this->em->getClassMetadata(get_class($entity));
       
  1653         foreach ($class->associationMappings as $assoc) {
       
  1654             if ( ! $assoc['isCascadeMerge']) {
       
  1655                 continue;
       
  1656             }
       
  1657             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
       
  1658             if ($relatedEntities instanceof Collection) {
       
  1659                 if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
       
  1660                     continue;
       
  1661                 }
       
  1662 
       
  1663                 if ($relatedEntities instanceof PersistentCollection) {
       
  1664                     // Unwrap so that foreach() does not initialize
       
  1665                     $relatedEntities = $relatedEntities->unwrap();
       
  1666                 }
       
  1667                 foreach ($relatedEntities as $relatedEntity) {
       
  1668                     $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
       
  1669                 }
       
  1670             } else if ($relatedEntities !== null) {
       
  1671                 $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
       
  1672             }
       
  1673         }
       
  1674     }
       
  1675 
       
  1676     /**
       
  1677      * Cascades the save operation to associated entities.
       
  1678      *
       
  1679      * @param object $entity
       
  1680      * @param array $visited
       
  1681      * @param array $insertNow
       
  1682      */
       
  1683     private function cascadePersist($entity, array &$visited)
       
  1684     {
       
  1685         $class = $this->em->getClassMetadata(get_class($entity));
       
  1686         foreach ($class->associationMappings as $assoc) {
       
  1687             if ( ! $assoc['isCascadePersist']) {
       
  1688                 continue;
       
  1689             }
       
  1690             
       
  1691             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
       
  1692             if (($relatedEntities instanceof Collection || is_array($relatedEntities))) {
       
  1693                 if ($relatedEntities instanceof PersistentCollection) {
       
  1694                     // Unwrap so that foreach() does not initialize
       
  1695                     $relatedEntities = $relatedEntities->unwrap();
       
  1696                 }
       
  1697                 foreach ($relatedEntities as $relatedEntity) {
       
  1698                     $this->doPersist($relatedEntity, $visited);
       
  1699                 }
       
  1700             } else if ($relatedEntities !== null) {
       
  1701                 $this->doPersist($relatedEntities, $visited);
       
  1702             }
       
  1703         }
       
  1704     }
       
  1705 
       
  1706     /**
       
  1707      * Cascades the delete operation to associated entities.
       
  1708      *
       
  1709      * @param object $entity
       
  1710      * @param array $visited
       
  1711      */
       
  1712     private function cascadeRemove($entity, array &$visited)
       
  1713     {
       
  1714         $class = $this->em->getClassMetadata(get_class($entity));
       
  1715         foreach ($class->associationMappings as $assoc) {
       
  1716             if ( ! $assoc['isCascadeRemove']) {
       
  1717                 continue;
       
  1718             }
       
  1719             
       
  1720             if ($entity instanceof Proxy && !$entity->__isInitialized__) {
       
  1721                 $entity->__load();
       
  1722             }
       
  1723             
       
  1724             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
       
  1725             if ($relatedEntities instanceof Collection || is_array($relatedEntities)) {
       
  1726                 // If its a PersistentCollection initialization is intended! No unwrap!
       
  1727                 foreach ($relatedEntities as $relatedEntity) {
       
  1728                     $this->doRemove($relatedEntity, $visited);
       
  1729                 }
       
  1730             } else if ($relatedEntities !== null) {
       
  1731                 $this->doRemove($relatedEntities, $visited);
       
  1732             }
       
  1733         }
       
  1734     }
       
  1735 
       
  1736     /**
       
  1737      * Acquire a lock on the given entity.
       
  1738      *
       
  1739      * @param object $entity
       
  1740      * @param int $lockMode
       
  1741      * @param int $lockVersion
       
  1742      */
       
  1743     public function lock($entity, $lockMode, $lockVersion = null)
       
  1744     {
       
  1745         if ($this->getEntityState($entity) != self::STATE_MANAGED) {
       
  1746             throw new InvalidArgumentException("Entity is not MANAGED.");
       
  1747         }
       
  1748         
       
  1749         $entityName = get_class($entity);
       
  1750         $class = $this->em->getClassMetadata($entityName);
       
  1751 
       
  1752         if ($lockMode == \Doctrine\DBAL\LockMode::OPTIMISTIC) {
       
  1753             if (!$class->isVersioned) {
       
  1754                 throw OptimisticLockException::notVersioned($entityName);
       
  1755             }
       
  1756 
       
  1757             if ($lockVersion != null) {
       
  1758                 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
       
  1759                 if ($entityVersion != $lockVersion) {
       
  1760                     throw OptimisticLockException::lockFailedVersionMissmatch($entity, $lockVersion, $entityVersion);
       
  1761                 }
       
  1762             }
       
  1763         } else if (in_array($lockMode, array(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE))) {
       
  1764 
       
  1765             if (!$this->em->getConnection()->isTransactionActive()) {
       
  1766                 throw TransactionRequiredException::transactionRequired();
       
  1767             }
       
  1768             
       
  1769             $oid = spl_object_hash($entity);
       
  1770 
       
  1771             $this->getEntityPersister($class->name)->lock(
       
  1772                 array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
       
  1773                 $lockMode
       
  1774             );
       
  1775         }
       
  1776     }
       
  1777 
       
  1778     /**
       
  1779      * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
       
  1780      *
       
  1781      * @return Doctrine\ORM\Internal\CommitOrderCalculator
       
  1782      */
       
  1783     public function getCommitOrderCalculator()
       
  1784     {
       
  1785         if ($this->commitOrderCalculator === null) {
       
  1786             $this->commitOrderCalculator = new Internal\CommitOrderCalculator;
       
  1787         }
       
  1788         return $this->commitOrderCalculator;
       
  1789     }
       
  1790 
       
  1791     /**
       
  1792      * Clears the UnitOfWork.
       
  1793      */
       
  1794     public function clear()
       
  1795     {
       
  1796         $this->identityMap =
       
  1797         $this->entityIdentifiers =
       
  1798         $this->originalEntityData =
       
  1799         $this->entityChangeSets =
       
  1800         $this->entityStates =
       
  1801         $this->scheduledForDirtyCheck =
       
  1802         $this->entityInsertions =
       
  1803         $this->entityUpdates =
       
  1804         $this->entityDeletions =
       
  1805         $this->collectionDeletions =
       
  1806         $this->collectionUpdates =
       
  1807         $this->extraUpdates =
       
  1808         $this->orphanRemovals = array();
       
  1809         if ($this->commitOrderCalculator !== null) {
       
  1810             $this->commitOrderCalculator->clear();
       
  1811         }
       
  1812 
       
  1813         if ($this->evm->hasListeners(Events::onClear)) {
       
  1814             $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em));
       
  1815         }
       
  1816     }
       
  1817     
       
  1818     /**
       
  1819      * INTERNAL:
       
  1820      * Schedules an orphaned entity for removal. The remove() operation will be
       
  1821      * invoked on that entity at the beginning of the next commit of this
       
  1822      * UnitOfWork.
       
  1823      * 
       
  1824      * @ignore
       
  1825      * @param object $entity
       
  1826      */
       
  1827     public function scheduleOrphanRemoval($entity)
       
  1828     {
       
  1829         $this->orphanRemovals[spl_object_hash($entity)] = $entity;
       
  1830     }
       
  1831     
       
  1832     /**
       
  1833      * INTERNAL:
       
  1834      * Schedules a complete collection for removal when this UnitOfWork commits.
       
  1835      *
       
  1836      * @param PersistentCollection $coll
       
  1837      */
       
  1838     public function scheduleCollectionDeletion(PersistentCollection $coll)
       
  1839     {
       
  1840         //TODO: if $coll is already scheduled for recreation ... what to do?
       
  1841         // Just remove $coll from the scheduled recreations?
       
  1842         $this->collectionDeletions[] = $coll;
       
  1843     }
       
  1844 
       
  1845     public function isCollectionScheduledForDeletion(PersistentCollection $coll)
       
  1846     {
       
  1847         return in_array($coll, $this->collectionsDeletions, true);
       
  1848     }
       
  1849 
       
  1850     /**
       
  1851      * INTERNAL:
       
  1852      * Creates an entity. Used for reconstitution of persistent entities.
       
  1853      *
       
  1854      * @ignore
       
  1855      * @param string $className The name of the entity class.
       
  1856      * @param array $data The data for the entity.
       
  1857      * @param array $hints Any hints to account for during reconstitution/lookup of the entity.
       
  1858      * @return object The managed entity instance.
       
  1859      * @internal Highly performance-sensitive method.
       
  1860      * 
       
  1861      * @todo Rename: getOrCreateEntity
       
  1862      */
       
  1863     public function createEntity($className, array $data, &$hints = array())
       
  1864     {
       
  1865         $class = $this->em->getClassMetadata($className);
       
  1866         //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
       
  1867 
       
  1868         if ($class->isIdentifierComposite) {
       
  1869             $id = array();
       
  1870             foreach ($class->identifier as $fieldName) {
       
  1871                 if (isset($class->associationMappings[$fieldName])) {
       
  1872                     $id[$fieldName] = $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']];
       
  1873                 } else {
       
  1874                     $id[$fieldName] = $data[$fieldName];
       
  1875                 }
       
  1876             }
       
  1877             $idHash = implode(' ', $id);
       
  1878         } else {
       
  1879             if (isset($class->associationMappings[$class->identifier[0]])) {
       
  1880                 $idHash = $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']];
       
  1881             } else {
       
  1882                 $idHash = $data[$class->identifier[0]];
       
  1883             }
       
  1884             $id = array($class->identifier[0] => $idHash);
       
  1885         }
       
  1886         
       
  1887         if (isset($this->identityMap[$class->rootEntityName][$idHash])) {            
       
  1888             $entity = $this->identityMap[$class->rootEntityName][$idHash];
       
  1889             $oid = spl_object_hash($entity);
       
  1890             if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
       
  1891                 $entity->__isInitialized__ = true;
       
  1892                 $overrideLocalValues = true;
       
  1893                 if ($entity instanceof NotifyPropertyChanged) {
       
  1894                     $entity->addPropertyChangedListener($this);
       
  1895                 }
       
  1896             } else {
       
  1897                 $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
       
  1898             }
       
  1899 
       
  1900             if ($overrideLocalValues) {
       
  1901                 $this->originalEntityData[$oid] = $data;
       
  1902             }
       
  1903         } else {
       
  1904             $entity = $class->newInstance();
       
  1905             $oid = spl_object_hash($entity);
       
  1906             $this->entityIdentifiers[$oid] = $id;
       
  1907             $this->entityStates[$oid] = self::STATE_MANAGED;
       
  1908             $this->originalEntityData[$oid] = $data;
       
  1909             $this->identityMap[$class->rootEntityName][$idHash] = $entity;
       
  1910             if ($entity instanceof NotifyPropertyChanged) {
       
  1911                 $entity->addPropertyChangedListener($this);
       
  1912             }
       
  1913             $overrideLocalValues = true;
       
  1914         }
       
  1915 
       
  1916         if ($overrideLocalValues) {
       
  1917             foreach ($data as $field => $value) {
       
  1918                 if (isset($class->fieldMappings[$field])) {
       
  1919                     $class->reflFields[$field]->setValue($entity, $value);
       
  1920                 }
       
  1921             }
       
  1922 
       
  1923             // Loading the entity right here, if its in the eager loading map get rid of it there.
       
  1924             unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
       
  1925             
       
  1926             // Properly initialize any unfetched associations, if partial objects are not allowed.
       
  1927             if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
       
  1928                 foreach ($class->associationMappings as $field => $assoc) {
       
  1929                     // Check if the association is not among the fetch-joined associations already.
       
  1930                     if (isset($hints['fetched'][$className][$field])) {
       
  1931                         continue;
       
  1932                     }
       
  1933 
       
  1934                     $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
       
  1935 
       
  1936                     if ($assoc['type'] & ClassMetadata::TO_ONE) {
       
  1937                         if ($assoc['isOwningSide']) {
       
  1938                             $associatedId = array();
       
  1939                             // TODO: Is this even computed right in all cases of composite keys?
       
  1940                             foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
       
  1941                                 $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null;
       
  1942                                 if ($joinColumnValue !== null) {
       
  1943                                     if ($targetClass->containsForeignIdentifier) {
       
  1944                                         $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
       
  1945                                     } else {
       
  1946                                         $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
       
  1947                                     }
       
  1948                                 }
       
  1949                             }
       
  1950                             if ( ! $associatedId) {
       
  1951                                 // Foreign key is NULL
       
  1952                                 $class->reflFields[$field]->setValue($entity, null);
       
  1953                                 $this->originalEntityData[$oid][$field] = null;
       
  1954                             } else {
       
  1955                                 if (!isset($hints['fetchMode'][$class->name][$field])) {
       
  1956                                     $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
       
  1957                                 }
       
  1958 
       
  1959                                 // Foreign key is set
       
  1960                                 // Check identity map first
       
  1961                                 // FIXME: Can break easily with composite keys if join column values are in
       
  1962                                 //        wrong order. The correct order is the one in ClassMetadata#identifier.
       
  1963                                 $relatedIdHash = implode(' ', $associatedId);
       
  1964                                 if (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])) {
       
  1965                                     $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
       
  1966                                     
       
  1967                                     // if this is an uninitialized proxy, we are deferring eager loads,
       
  1968                                     // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
       
  1969                                     // then we cann append this entity for eager loading!
       
  1970                                     if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
       
  1971                                         isset($hints['deferEagerLoad']) &&
       
  1972                                         !$targetClass->isIdentifierComposite &&
       
  1973                                         $newValue instanceof Proxy &&
       
  1974                                         $newValue->__isInitialized__ === false) {
       
  1975                                         
       
  1976                                         $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
       
  1977                                     }
       
  1978                                 } else {
       
  1979                                     if ($targetClass->subClasses) {
       
  1980                                         // If it might be a subtype, it can not be lazy. There isn't even
       
  1981                                         // a way to solve this with deferred eager loading, which means putting
       
  1982                                         // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
       
  1983                                         $newValue = $this->getEntityPersister($assoc['targetEntity'])
       
  1984                                                 ->loadOneToOneEntity($assoc, $entity, $associatedId);
       
  1985                                     } else {
       
  1986                                         // Deferred eager load only works for single identifier classes
       
  1987 
       
  1988                                         if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER) {
       
  1989                                             if (isset($hints['deferEagerLoad']) && !$targetClass->isIdentifierComposite) {
       
  1990                                                 // TODO: Is there a faster approach?
       
  1991                                                 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
       
  1992 
       
  1993                                                 $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
       
  1994                                             } else {
       
  1995                                                 // TODO: This is very imperformant, ignore it?
       
  1996                                                 $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
       
  1997                                             }
       
  1998                                         } else {
       
  1999                                             $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
       
  2000                                         }
       
  2001                                         // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
       
  2002                                         $newValueOid = spl_object_hash($newValue);
       
  2003                                         $this->entityIdentifiers[$newValueOid] = $associatedId;
       
  2004                                         $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
       
  2005                                         $this->entityStates[$newValueOid] = self::STATE_MANAGED;
       
  2006                                         // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
       
  2007                                     }
       
  2008                                 }
       
  2009                                 $this->originalEntityData[$oid][$field] = $newValue;
       
  2010                                 $class->reflFields[$field]->setValue($entity, $newValue);
       
  2011                                 
       
  2012                                 if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
       
  2013                                     $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
       
  2014                                     $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
       
  2015                                 }
       
  2016                             }
       
  2017                         } else {
       
  2018                             // Inverse side of x-to-one can never be lazy
       
  2019                             $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])
       
  2020                                     ->loadOneToOneEntity($assoc, $entity));
       
  2021                         }
       
  2022                     } else {
       
  2023                         // Inject collection
       
  2024                         $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
       
  2025                         $pColl->setOwner($entity, $assoc);
       
  2026 
       
  2027                         $reflField = $class->reflFields[$field];
       
  2028                         $reflField->setValue($entity, $pColl);
       
  2029 
       
  2030                         if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
       
  2031                             $this->loadCollection($pColl);
       
  2032                             $pColl->takeSnapshot();
       
  2033                         } else {
       
  2034                             $pColl->setInitialized(false);
       
  2035                         }
       
  2036                         $this->originalEntityData[$oid][$field] = $pColl;
       
  2037                     }
       
  2038                 }
       
  2039             }
       
  2040         }
       
  2041         
       
  2042         //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here.
       
  2043         if (isset($class->lifecycleCallbacks[Events::postLoad])) {
       
  2044             $class->invokeLifecycleCallbacks(Events::postLoad, $entity);
       
  2045         }
       
  2046         if ($this->evm->hasListeners(Events::postLoad)) {
       
  2047             $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em));
       
  2048         }
       
  2049 
       
  2050         return $entity;
       
  2051     }
       
  2052 
       
  2053     /**
       
  2054      * @return void
       
  2055      */
       
  2056     public function triggerEagerLoads()
       
  2057     {
       
  2058         if (!$this->eagerLoadingEntities) {
       
  2059             return;
       
  2060         }
       
  2061 
       
  2062         // avoid infinite recursion
       
  2063         $eagerLoadingEntities = $this->eagerLoadingEntities;
       
  2064         $this->eagerLoadingEntities = array();
       
  2065 
       
  2066         foreach ($eagerLoadingEntities AS $entityName => $ids) {
       
  2067             $class = $this->em->getClassMetadata($entityName);
       
  2068             $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, array(array_values($ids))));
       
  2069         }
       
  2070     }
       
  2071 
       
  2072     /**
       
  2073      * Initializes (loads) an uninitialized persistent collection of an entity.
       
  2074      *
       
  2075      * @param PeristentCollection $collection The collection to initialize.
       
  2076      * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
       
  2077      */
       
  2078     public function loadCollection(PersistentCollection $collection)
       
  2079     {
       
  2080         $assoc = $collection->getMapping();
       
  2081         switch ($assoc['type']) {
       
  2082             case ClassMetadata::ONE_TO_MANY:
       
  2083                 $this->getEntityPersister($assoc['targetEntity'])->loadOneToManyCollection(
       
  2084                         $assoc, $collection->getOwner(), $collection);
       
  2085                 break;
       
  2086             case ClassMetadata::MANY_TO_MANY:
       
  2087                 $this->getEntityPersister($assoc['targetEntity'])->loadManyToManyCollection(
       
  2088                         $assoc, $collection->getOwner(), $collection);
       
  2089                 break;
       
  2090         }
       
  2091     }
       
  2092 
       
  2093     /**
       
  2094      * Gets the identity map of the UnitOfWork.
       
  2095      *
       
  2096      * @return array
       
  2097      */
       
  2098     public function getIdentityMap()
       
  2099     {
       
  2100         return $this->identityMap;
       
  2101     }
       
  2102 
       
  2103     /**
       
  2104      * Gets the original data of an entity. The original data is the data that was
       
  2105      * present at the time the entity was reconstituted from the database.
       
  2106      *
       
  2107      * @param object $entity
       
  2108      * @return array
       
  2109      */
       
  2110     public function getOriginalEntityData($entity)
       
  2111     {
       
  2112         $oid = spl_object_hash($entity);
       
  2113         if (isset($this->originalEntityData[$oid])) {
       
  2114             return $this->originalEntityData[$oid];
       
  2115         }
       
  2116         return array();
       
  2117     }
       
  2118     
       
  2119     /**
       
  2120      * @ignore
       
  2121      */
       
  2122     public function setOriginalEntityData($entity, array $data)
       
  2123     {
       
  2124         $this->originalEntityData[spl_object_hash($entity)] = $data;
       
  2125     }
       
  2126 
       
  2127     /**
       
  2128      * INTERNAL:
       
  2129      * Sets a property value of the original data array of an entity.
       
  2130      *
       
  2131      * @ignore
       
  2132      * @param string $oid
       
  2133      * @param string $property
       
  2134      * @param mixed $value
       
  2135      */
       
  2136     public function setOriginalEntityProperty($oid, $property, $value)
       
  2137     {
       
  2138         $this->originalEntityData[$oid][$property] = $value;
       
  2139     }
       
  2140 
       
  2141     /**
       
  2142      * Gets the identifier of an entity.
       
  2143      * The returned value is always an array of identifier values. If the entity
       
  2144      * has a composite identifier then the identifier values are in the same
       
  2145      * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
       
  2146      *
       
  2147      * @param object $entity
       
  2148      * @return array The identifier values.
       
  2149      */
       
  2150     public function getEntityIdentifier($entity)
       
  2151     {        
       
  2152         return $this->entityIdentifiers[spl_object_hash($entity)];
       
  2153     }
       
  2154 
       
  2155     /**
       
  2156      * Tries to find an entity with the given identifier in the identity map of
       
  2157      * this UnitOfWork.
       
  2158      *
       
  2159      * @param mixed $id The entity identifier to look for.
       
  2160      * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
       
  2161      * @return mixed Returns the entity with the specified identifier if it exists in
       
  2162      *               this UnitOfWork, FALSE otherwise.
       
  2163      */
       
  2164     public function tryGetById($id, $rootClassName)
       
  2165     {
       
  2166         $idHash = implode(' ', (array) $id);
       
  2167         if (isset($this->identityMap[$rootClassName][$idHash])) {
       
  2168             return $this->identityMap[$rootClassName][$idHash];
       
  2169         }
       
  2170         return false;
       
  2171     }
       
  2172 
       
  2173     /**
       
  2174      * Schedules an entity for dirty-checking at commit-time.
       
  2175      *
       
  2176      * @param object $entity The entity to schedule for dirty-checking.
       
  2177      * @todo Rename: scheduleForSynchronization
       
  2178      */
       
  2179     public function scheduleForDirtyCheck($entity)
       
  2180     {
       
  2181         $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
       
  2182         $this->scheduledForDirtyCheck[$rootClassName][spl_object_hash($entity)] = $entity;
       
  2183     }
       
  2184 
       
  2185     /**
       
  2186      * Checks whether the UnitOfWork has any pending insertions.
       
  2187      *
       
  2188      * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
       
  2189      */
       
  2190     public function hasPendingInsertions()
       
  2191     {
       
  2192         return ! empty($this->entityInsertions);
       
  2193     }
       
  2194 
       
  2195     /**
       
  2196      * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
       
  2197      * number of entities in the identity map.
       
  2198      *
       
  2199      * @return integer
       
  2200      */
       
  2201     public function size()
       
  2202     {
       
  2203         $count = 0;
       
  2204         foreach ($this->identityMap as $entitySet) {
       
  2205             $count += count($entitySet);
       
  2206         }
       
  2207         return $count;
       
  2208     }
       
  2209 
       
  2210     /**
       
  2211      * Gets the EntityPersister for an Entity.
       
  2212      *
       
  2213      * @param string $entityName  The name of the Entity.
       
  2214      * @return Doctrine\ORM\Persisters\AbstractEntityPersister
       
  2215      */
       
  2216     public function getEntityPersister($entityName)
       
  2217     {
       
  2218         if ( ! isset($this->persisters[$entityName])) {
       
  2219             $class = $this->em->getClassMetadata($entityName);
       
  2220             if ($class->isInheritanceTypeNone()) {
       
  2221                 $persister = new Persisters\BasicEntityPersister($this->em, $class);
       
  2222             } else if ($class->isInheritanceTypeSingleTable()) {
       
  2223                 $persister = new Persisters\SingleTablePersister($this->em, $class);
       
  2224             } else if ($class->isInheritanceTypeJoined()) {
       
  2225                 $persister = new Persisters\JoinedSubclassPersister($this->em, $class);
       
  2226             } else {
       
  2227                 $persister = new Persisters\UnionSubclassPersister($this->em, $class);
       
  2228             }
       
  2229             $this->persisters[$entityName] = $persister;
       
  2230         }
       
  2231         return $this->persisters[$entityName];
       
  2232     }
       
  2233 
       
  2234     /**
       
  2235      * Gets a collection persister for a collection-valued association.
       
  2236      *
       
  2237      * @param AssociationMapping $association
       
  2238      * @return AbstractCollectionPersister
       
  2239      */
       
  2240     public function getCollectionPersister(array $association)
       
  2241     {
       
  2242         $type = $association['type'];
       
  2243         if ( ! isset($this->collectionPersisters[$type])) {
       
  2244             if ($type == ClassMetadata::ONE_TO_MANY) {
       
  2245                 $persister = new Persisters\OneToManyPersister($this->em);
       
  2246             } else if ($type == ClassMetadata::MANY_TO_MANY) {
       
  2247                 $persister = new Persisters\ManyToManyPersister($this->em);
       
  2248             }
       
  2249             $this->collectionPersisters[$type] = $persister;
       
  2250         }
       
  2251         return $this->collectionPersisters[$type];
       
  2252     }
       
  2253 
       
  2254     /**
       
  2255      * INTERNAL:
       
  2256      * Registers an entity as managed.
       
  2257      *
       
  2258      * @param object $entity The entity.
       
  2259      * @param array $id The identifier values.
       
  2260      * @param array $data The original entity data.
       
  2261      */
       
  2262     public function registerManaged($entity, array $id, array $data)
       
  2263     {
       
  2264         $oid = spl_object_hash($entity);
       
  2265         $this->entityIdentifiers[$oid] = $id;
       
  2266         $this->entityStates[$oid] = self::STATE_MANAGED;
       
  2267         $this->originalEntityData[$oid] = $data;
       
  2268         $this->addToIdentityMap($entity);
       
  2269     }
       
  2270 
       
  2271     /**
       
  2272      * INTERNAL:
       
  2273      * Clears the property changeset of the entity with the given OID.
       
  2274      *
       
  2275      * @param string $oid The entity's OID.
       
  2276      */
       
  2277     public function clearEntityChangeSet($oid)
       
  2278     {
       
  2279         $this->entityChangeSets[$oid] = array();
       
  2280     }
       
  2281 
       
  2282     /* PropertyChangedListener implementation */
       
  2283 
       
  2284     /**
       
  2285      * Notifies this UnitOfWork of a property change in an entity.
       
  2286      *
       
  2287      * @param object $entity The entity that owns the property.
       
  2288      * @param string $propertyName The name of the property that changed.
       
  2289      * @param mixed $oldValue The old value of the property.
       
  2290      * @param mixed $newValue The new value of the property.
       
  2291      */
       
  2292     public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
       
  2293     {
       
  2294         $oid = spl_object_hash($entity);
       
  2295         $class = $this->em->getClassMetadata(get_class($entity));
       
  2296 
       
  2297         $isAssocField = isset($class->associationMappings[$propertyName]);
       
  2298 
       
  2299         if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
       
  2300             return; // ignore non-persistent fields
       
  2301         }
       
  2302 
       
  2303         // Update changeset and mark entity for synchronization
       
  2304         $this->entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
       
  2305         if ( ! isset($this->scheduledForDirtyCheck[$class->rootEntityName][$oid])) {
       
  2306             $this->scheduleForDirtyCheck($entity);
       
  2307         }
       
  2308     }
       
  2309 
       
  2310     /**
       
  2311      * Gets the currently scheduled entity insertions in this UnitOfWork.
       
  2312      * 
       
  2313      * @return array
       
  2314      */
       
  2315     public function getScheduledEntityInsertions()
       
  2316     {
       
  2317         return $this->entityInsertions;
       
  2318     }
       
  2319     
       
  2320     /**
       
  2321      * Gets the currently scheduled entity updates in this UnitOfWork.
       
  2322      * 
       
  2323      * @return array
       
  2324      */
       
  2325     public function getScheduledEntityUpdates()
       
  2326     {
       
  2327         return $this->entityUpdates;
       
  2328     }
       
  2329     
       
  2330     /**
       
  2331      * Gets the currently scheduled entity deletions in this UnitOfWork.
       
  2332      * 
       
  2333      * @return array
       
  2334      */
       
  2335     public function getScheduledEntityDeletions()
       
  2336     {
       
  2337         return $this->entityDeletions;
       
  2338     }
       
  2339 
       
  2340     /**
       
  2341      * Get the currently scheduled complete collection deletions
       
  2342      *
       
  2343      * @return array
       
  2344      */
       
  2345     public function getScheduledCollectionDeletions()
       
  2346     {
       
  2347         return $this->collectionDeletions;
       
  2348     }
       
  2349 
       
  2350     /**
       
  2351      * Gets the currently scheduled collection inserts, updates and deletes.
       
  2352      *
       
  2353      * @return array
       
  2354      */
       
  2355     public function getScheduledCollectionUpdates()
       
  2356     {
       
  2357         return $this->collectionUpdates;
       
  2358     }
       
  2359     
       
  2360     /**
       
  2361      * Helper method to initialize a lazy loading proxy or persistent collection.
       
  2362      * 
       
  2363      * @param object
       
  2364      * @return void
       
  2365      */
       
  2366     public function initializeObject($obj)
       
  2367     {
       
  2368         if ($obj instanceof Proxy) {
       
  2369             $obj->__load();
       
  2370         } else if ($obj instanceof PersistentCollection) {
       
  2371             $obj->initialize();
       
  2372         }
       
  2373     }
       
  2374     
       
  2375     /**
       
  2376      * Helper method to show an object as string.
       
  2377      * 
       
  2378      * @param  object $obj
       
  2379      * @return string 
       
  2380      */
       
  2381     private static function objToStr($obj)
       
  2382     {
       
  2383         return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj);
       
  2384     }
       
  2385 }