Rajout de doctrine/orm
[zf2.biz/galerie.git] / vendor / doctrine / orm / lib / Doctrine / ORM / UnitOfWork.php
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 MIT license. 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\Common\Persistence\ObjectManagerAware,
28     Doctrine\ORM\Event\LifecycleEventArgs,
29     Doctrine\ORM\Mapping\ClassMetadata,
30     Doctrine\ORM\Proxy\Proxy;
31
32 /**
33  * The UnitOfWork is responsible for tracking changes to objects during an
34  * "object-level" transaction and for writing out changes to the database
35  * in the correct order.
36  *
37  * @since       2.0
38  * @author      Benjamin Eberlei <kontakt@beberlei.de>
39  * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
40  * @author      Jonathan Wage <jonwage@gmail.com>
41  * @author      Roman Borschel <roman@code-factory.org>
42  * @internal    This class contains highly performance-sensitive code.
43  */
44 class UnitOfWork implements PropertyChangedListener
45 {
46     /**
47      * An entity is in MANAGED state when its persistence is managed by an EntityManager.
48      */
49     const STATE_MANAGED = 1;
50
51     /**
52      * An entity is new if it has just been instantiated (i.e. using the "new" operator)
53      * and is not (yet) managed by an EntityManager.
54      */
55     const STATE_NEW = 2;
56
57     /**
58      * A detached entity is an instance with persistent state and identity that is not
59      * (or no longer) associated with an EntityManager (and a UnitOfWork).
60      */
61     const STATE_DETACHED = 3;
62
63     /**
64      * A removed entity instance is an instance with a persistent identity,
65      * associated with an EntityManager, whose persistent state will be deleted
66      * on commit.
67      */
68     const STATE_REMOVED = 4;
69
70     /**
71      * The identity map that holds references to all managed entities that have
72      * an identity. The entities are grouped by their class name.
73      * Since all classes in a hierarchy must share the same identifier set,
74      * we always take the root class name of the hierarchy.
75      *
76      * @var array
77      */
78     private $identityMap = array();
79
80     /**
81      * Map of all identifiers of managed entities.
82      * Keys are object ids (spl_object_hash).
83      *
84      * @var array
85      */
86     private $entityIdentifiers = array();
87
88     /**
89      * Map of the original entity data of managed entities.
90      * Keys are object ids (spl_object_hash). This is used for calculating changesets
91      * at commit time.
92      *
93      * @var array
94      * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
95      *           A value will only really be copied if the value in the entity is modified
96      *           by the user.
97      */
98     private $originalEntityData = array();
99
100     /**
101      * Map of entity changes. Keys are object ids (spl_object_hash).
102      * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
103      *
104      * @var array
105      */
106     private $entityChangeSets = array();
107
108     /**
109      * The (cached) states of any known entities.
110      * Keys are object ids (spl_object_hash).
111      *
112      * @var array
113      */
114     private $entityStates = array();
115
116     /**
117      * Map of entities that are scheduled for dirty checking at commit time.
118      * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
119      * Keys are object ids (spl_object_hash).
120      *
121      * @var array
122      * @todo rename: scheduledForSynchronization
123      */
124     private $scheduledForDirtyCheck = array();
125
126     /**
127      * A list of all pending entity insertions.
128      *
129      * @var array
130      */
131     private $entityInsertions = array();
132
133     /**
134      * A list of all pending entity updates.
135      *
136      * @var array
137      */
138     private $entityUpdates = array();
139
140     /**
141      * Any pending extra updates that have been scheduled by persisters.
142      *
143      * @var array
144      */
145     private $extraUpdates = array();
146
147     /**
148      * A list of all pending entity deletions.
149      *
150      * @var array
151      */
152     private $entityDeletions = array();
153
154     /**
155      * All pending collection deletions.
156      *
157      * @var array
158      */
159     private $collectionDeletions = array();
160
161     /**
162      * All pending collection updates.
163      *
164      * @var array
165      */
166     private $collectionUpdates = array();
167
168     /**
169      * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
170      * At the end of the UnitOfWork all these collections will make new snapshots
171      * of their data.
172      *
173      * @var array
174      */
175     private $visitedCollections = array();
176
177     /**
178      * The EntityManager that "owns" this UnitOfWork instance.
179      *
180      * @var \Doctrine\ORM\EntityManager
181      */
182     private $em;
183
184     /**
185      * The calculator used to calculate the order in which changes to
186      * entities need to be written to the database.
187      *
188      * @var \Doctrine\ORM\Internal\CommitOrderCalculator
189      */
190     private $commitOrderCalculator;
191
192     /**
193      * The entity persister instances used to persist entity instances.
194      *
195      * @var array
196      */
197     private $persisters = array();
198
199     /**
200      * The collection persister instances used to persist collections.
201      *
202      * @var array
203      */
204     private $collectionPersisters = array();
205
206     /**
207      * The EventManager used for dispatching events.
208      *
209      * @var \Doctrine\Common\EventManager
210      */
211     private $evm;
212
213     /**
214      * Orphaned entities that are scheduled for removal.
215      *
216      * @var array
217      */
218     private $orphanRemovals = array();
219
220     /**
221      * Read-Only objects are never evaluated
222      *
223      * @var array
224      */
225     private $readOnlyObjects = array();
226
227     /**
228      * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
229      *
230      * @var array
231      */
232     private $eagerLoadingEntities = array();
233
234     /**
235      * Initializes a new UnitOfWork instance, bound to the given EntityManager.
236      *
237      * @param \Doctrine\ORM\EntityManager $em
238      */
239     public function __construct(EntityManager $em)
240     {
241         $this->em = $em;
242         $this->evm = $em->getEventManager();
243     }
244
245     /**
246      * Commits the UnitOfWork, executing all operations that have been postponed
247      * up to this point. The state of all managed entities will be synchronized with
248      * the database.
249      *
250      * The operations are executed in the following order:
251      *
252      * 1) All entity insertions
253      * 2) All entity updates
254      * 3) All collection deletions
255      * 4) All collection updates
256      * 5) All entity deletions
257      *
258      * @param null|object|array $entity
259      *
260      * @throws \Exception
261      *
262      * @return void
263      */
264     public function commit($entity = null)
265     {
266         // Raise preFlush
267         if ($this->evm->hasListeners(Events::preFlush)) {
268             $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->em));
269         }
270
271         // Compute changes done since last commit.
272         if ($entity === null) {
273             $this->computeChangeSets();
274         } elseif (is_object($entity)) {
275             $this->computeSingleEntityChangeSet($entity);
276         } elseif (is_array($entity)) {
277             foreach ($entity as $object) {
278                 $this->computeSingleEntityChangeSet($object);
279             }
280         }
281
282         if ( ! ($this->entityInsertions ||
283                 $this->entityDeletions ||
284                 $this->entityUpdates ||
285                 $this->collectionUpdates ||
286                 $this->collectionDeletions ||
287                 $this->orphanRemovals)) {
288             return; // Nothing to do.
289         }
290
291         if ($this->orphanRemovals) {
292             foreach ($this->orphanRemovals as $orphan) {
293                 $this->remove($orphan);
294             }
295         }
296
297         // Raise onFlush
298         if ($this->evm->hasListeners(Events::onFlush)) {
299             $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->em));
300         }
301
302         // Now we need a commit order to maintain referential integrity
303         $commitOrder = $this->getCommitOrder();
304
305         $conn = $this->em->getConnection();
306         $conn->beginTransaction();
307
308         try {
309             if ($this->entityInsertions) {
310                 foreach ($commitOrder as $class) {
311                     $this->executeInserts($class);
312                 }
313             }
314
315             if ($this->entityUpdates) {
316                 foreach ($commitOrder as $class) {
317                     $this->executeUpdates($class);
318                 }
319             }
320
321             // Extra updates that were requested by persisters.
322             if ($this->extraUpdates) {
323                 $this->executeExtraUpdates();
324             }
325
326             // Collection deletions (deletions of complete collections)
327             foreach ($this->collectionDeletions as $collectionToDelete) {
328                 $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
329             }
330             // Collection updates (deleteRows, updateRows, insertRows)
331             foreach ($this->collectionUpdates as $collectionToUpdate) {
332                 $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
333             }
334
335             // Entity deletions come last and need to be in reverse commit order
336             if ($this->entityDeletions) {
337                 for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
338                     $this->executeDeletions($commitOrder[$i]);
339                 }
340             }
341
342             $conn->commit();
343         } catch (Exception $e) {
344             $this->em->close();
345             $conn->rollback();
346
347             throw $e;
348         }
349
350         // Take new snapshots from visited collections
351         foreach ($this->visitedCollections as $coll) {
352             $coll->takeSnapshot();
353         }
354
355         // Raise postFlush
356         if ($this->evm->hasListeners(Events::postFlush)) {
357             $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->em));
358         }
359
360         // Clear up
361         $this->entityInsertions =
362         $this->entityUpdates =
363         $this->entityDeletions =
364         $this->extraUpdates =
365         $this->entityChangeSets =
366         $this->collectionUpdates =
367         $this->collectionDeletions =
368         $this->visitedCollections =
369         $this->scheduledForDirtyCheck =
370         $this->orphanRemovals = array();
371     }
372
373     /**
374      * Compute the changesets of all entities scheduled for insertion
375      *
376      * @return void
377      */
378     private function computeScheduleInsertsChangeSets()
379     {
380         foreach ($this->entityInsertions as $entity) {
381             $class = $this->em->getClassMetadata(get_class($entity));
382
383             $this->computeChangeSet($class, $entity);
384         }
385     }
386
387     /**
388      * Only flush the given entity according to a ruleset that keeps the UoW consistent.
389      *
390      * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
391      * 2. Read Only entities are skipped.
392      * 3. Proxies are skipped.
393      * 4. Only if entity is properly managed.
394      *
395      * @param  object $entity
396      *
397      * @throws \InvalidArgumentException
398      *
399      * @return void
400      */
401     private function computeSingleEntityChangeSet($entity)
402     {
403         if ( $this->getEntityState($entity) !== self::STATE_MANAGED) {
404             throw new \InvalidArgumentException("Entity has to be managed for single computation " . self::objToStr($entity));
405         }
406
407         $class = $this->em->getClassMetadata(get_class($entity));
408
409         if ($class->isChangeTrackingDeferredImplicit()) {
410             $this->persist($entity);
411         }
412
413         // Compute changes for INSERTed entities first. This must always happen even in this case.
414         $this->computeScheduleInsertsChangeSets();
415
416         if ($class->isReadOnly) {
417             return;
418         }
419
420         // Ignore uninitialized proxy objects
421         if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
422             return;
423         }
424
425         // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
426         $oid = spl_object_hash($entity);
427
428         if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) {
429             $this->computeChangeSet($class, $entity);
430         }
431     }
432
433     /**
434      * Executes any extra updates that have been scheduled.
435      */
436     private function executeExtraUpdates()
437     {
438         foreach ($this->extraUpdates as $oid => $update) {
439             list ($entity, $changeset) = $update;
440
441             $this->entityChangeSets[$oid] = $changeset;
442             $this->getEntityPersister(get_class($entity))->update($entity);
443         }
444     }
445
446     /**
447      * Gets the changeset for an entity.
448      *
449      * @param object $entity
450      *
451      * @return array
452      */
453     public function getEntityChangeSet($entity)
454     {
455         $oid = spl_object_hash($entity);
456
457         if (isset($this->entityChangeSets[$oid])) {
458             return $this->entityChangeSets[$oid];
459         }
460
461         return array();
462     }
463
464     /**
465      * Computes the changes that happened to a single entity.
466      *
467      * Modifies/populates the following properties:
468      *
469      * {@link _originalEntityData}
470      * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
471      * then it was not fetched from the database and therefore we have no original
472      * entity data yet. All of the current entity data is stored as the original entity data.
473      *
474      * {@link _entityChangeSets}
475      * The changes detected on all properties of the entity are stored there.
476      * A change is a tuple array where the first entry is the old value and the second
477      * entry is the new value of the property. Changesets are used by persisters
478      * to INSERT/UPDATE the persistent entity state.
479      *
480      * {@link _entityUpdates}
481      * If the entity is already fully MANAGED (has been fetched from the database before)
482      * and any changes to its properties are detected, then a reference to the entity is stored
483      * there to mark it for an update.
484      *
485      * {@link _collectionDeletions}
486      * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
487      * then this collection is marked for deletion.
488      *
489      * @ignore
490      * @internal Don't call from the outside.
491      * @param ClassMetadata $class The class descriptor of the entity.
492      * @param object $entity The entity for which to compute the changes.
493      */
494     public function computeChangeSet(ClassMetadata $class, $entity)
495     {
496         $oid = spl_object_hash($entity);
497
498         if (isset($this->readOnlyObjects[$oid])) {
499             return;
500         }
501
502         if ( ! $class->isInheritanceTypeNone()) {
503             $class = $this->em->getClassMetadata(get_class($entity));
504         }
505
506         // Fire PreFlush lifecycle callbacks
507         if (isset($class->lifecycleCallbacks[Events::preFlush])) {
508             $class->invokeLifecycleCallbacks(Events::preFlush, $entity);
509         }
510
511         $actualData = array();
512
513         foreach ($class->reflFields as $name => $refProp) {
514             $value = $refProp->getValue($entity);
515
516             if ($class->isCollectionValuedAssociation($name) && $value !== null && ! ($value instanceof PersistentCollection)) {
517                 // If $value is not a Collection then use an ArrayCollection.
518                 if ( ! $value instanceof Collection) {
519                     $value = new ArrayCollection($value);
520                 }
521
522                 $assoc = $class->associationMappings[$name];
523
524                 // Inject PersistentCollection
525                 $value = new PersistentCollection(
526                     $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
527                 );
528                 $value->setOwner($entity, $assoc);
529                 $value->setDirty( ! $value->isEmpty());
530
531                 $class->reflFields[$name]->setValue($entity, $value);
532
533                 $actualData[$name] = $value;
534
535                 continue;
536             }
537
538             if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
539                 $actualData[$name] = $value;
540             }
541         }
542
543         if ( ! isset($this->originalEntityData[$oid])) {
544             // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
545             // These result in an INSERT.
546             $this->originalEntityData[$oid] = $actualData;
547             $changeSet = array();
548
549             foreach ($actualData as $propName => $actualValue) {
550                 if ( ! isset($class->associationMappings[$propName])) {
551                     $changeSet[$propName] = array(null, $actualValue);
552
553                     continue;
554                 }
555
556                 $assoc = $class->associationMappings[$propName];
557
558                 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
559                     $changeSet[$propName] = array(null, $actualValue);
560                 }
561             }
562
563             $this->entityChangeSets[$oid] = $changeSet;
564         } else {
565             // Entity is "fully" MANAGED: it was already fully persisted before
566             // and we have a copy of the original data
567             $originalData           = $this->originalEntityData[$oid];
568             $isChangeTrackingNotify = $class->isChangeTrackingNotify();
569             $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
570                 ? $this->entityChangeSets[$oid]
571                 : array();
572
573             foreach ($actualData as $propName => $actualValue) {
574                 // skip field, its a partially omitted one!
575                 if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
576                     continue;
577                 }
578
579                 $orgValue = $originalData[$propName];
580
581                 // skip if value havent changed
582                 if ($orgValue === $actualValue) {
583                     continue;
584                 }
585
586                 // if regular field
587                 if ( ! isset($class->associationMappings[$propName])) {
588                     if ($isChangeTrackingNotify) {
589                         continue;
590                     }
591
592                     $changeSet[$propName] = array($orgValue, $actualValue);
593
594                     continue;
595                 }
596
597                 $assoc = $class->associationMappings[$propName];
598
599                 // Persistent collection was exchanged with the "originally"
600                 // created one. This can only mean it was cloned and replaced
601                 // on another entity.
602                 if ($actualValue instanceof PersistentCollection) {
603                     $owner = $actualValue->getOwner();
604                     if ($owner === null) { // cloned
605                         $actualValue->setOwner($entity, $assoc);
606                     } else if ($owner !== $entity) { // no clone, we have to fix
607                         if (!$actualValue->isInitialized()) {
608                             $actualValue->initialize(); // we have to do this otherwise the cols share state
609                         }
610                         $newValue = clone $actualValue;
611                         $newValue->setOwner($entity, $assoc);
612                         $class->reflFields[$propName]->setValue($entity, $newValue);
613                     }
614                 }
615
616                 if ($orgValue instanceof PersistentCollection) {
617                     // A PersistentCollection was de-referenced, so delete it.
618                     $coid = spl_object_hash($orgValue);
619
620                     if (isset($this->collectionDeletions[$coid])) {
621                         continue;
622                     }
623
624                     $this->collectionDeletions[$coid] = $orgValue;
625                     $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
626
627                     continue;
628                 }
629
630                 if ($assoc['type'] & ClassMetadata::TO_ONE) {
631                     if ($assoc['isOwningSide']) {
632                         $changeSet[$propName] = array($orgValue, $actualValue);
633                     }
634
635                     if ($orgValue !== null && $assoc['orphanRemoval']) {
636                         $this->scheduleOrphanRemoval($orgValue);
637                     }
638                 }
639             }
640
641             if ($changeSet) {
642                 $this->entityChangeSets[$oid]   = $changeSet;
643                 $this->originalEntityData[$oid] = $actualData;
644                 $this->entityUpdates[$oid]      = $entity;
645             }
646         }
647
648         // Look for changes in associations of the entity
649         foreach ($class->associationMappings as $field => $assoc) {
650             if (($val = $class->reflFields[$field]->getValue($entity)) !== null) {
651                 $this->computeAssociationChanges($assoc, $val);
652                 if (!isset($this->entityChangeSets[$oid]) &&
653                     $assoc['isOwningSide'] &&
654                     $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
655                     $val instanceof PersistentCollection &&
656                     $val->isDirty()) {
657                     $this->entityChangeSets[$oid]   = array();
658                     $this->originalEntityData[$oid] = $actualData;
659                     $this->entityUpdates[$oid]      = $entity;
660                 }
661             }
662         }
663     }
664
665     /**
666      * Computes all the changes that have been done to entities and collections
667      * since the last commit and stores these changes in the _entityChangeSet map
668      * temporarily for access by the persisters, until the UoW commit is finished.
669      */
670     public function computeChangeSets()
671     {
672         // Compute changes for INSERTed entities first. This must always happen.
673         $this->computeScheduleInsertsChangeSets();
674
675         // Compute changes for other MANAGED entities. Change tracking policies take effect here.
676         foreach ($this->identityMap as $className => $entities) {
677             $class = $this->em->getClassMetadata($className);
678
679             // Skip class if instances are read-only
680             if ($class->isReadOnly) {
681                 continue;
682             }
683
684             // If change tracking is explicit or happens through notification, then only compute
685             // changes on entities of that type that are explicitly marked for synchronization.
686             switch (true) {
687                 case ($class->isChangeTrackingDeferredImplicit()):
688                     $entitiesToProcess = $entities;
689                     break;
690
691                 case (isset($this->scheduledForDirtyCheck[$className])):
692                     $entitiesToProcess = $this->scheduledForDirtyCheck[$className];
693                     break;
694
695                 default:
696                     $entitiesToProcess = array();
697
698             }
699
700             foreach ($entitiesToProcess as $entity) {
701                 // Ignore uninitialized proxy objects
702                 if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
703                     continue;
704                 }
705
706                 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
707                 $oid = spl_object_hash($entity);
708
709                 if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) {
710                     $this->computeChangeSet($class, $entity);
711                 }
712             }
713         }
714     }
715
716     /**
717      * Computes the changes of an association.
718      *
719      * @param array $assoc
720      * @param mixed $value The value of the association.
721      *
722      * @throws ORMInvalidArgumentException
723      * @throws ORMException
724      *
725      * @return void
726      */
727     private function computeAssociationChanges($assoc, $value)
728     {
729         if ($value instanceof Proxy && ! $value->__isInitialized__) {
730             return;
731         }
732
733         if ($value instanceof PersistentCollection && $value->isDirty()) {
734             $coid = spl_object_hash($value);
735
736             if ($assoc['isOwningSide']) {
737                 $this->collectionUpdates[$coid] = $value;
738             }
739
740             $this->visitedCollections[$coid] = $value;
741         }
742
743         // Look through the entities, and in any of their associations,
744         // for transient (new) entities, recursively. ("Persistence by reachability")
745         // Unwrap. Uninitialized collections will simply be empty.
746         $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? array($value) : $value->unwrap();
747         $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
748
749         foreach ($unwrappedValue as $key => $entry) {
750             $state = $this->getEntityState($entry, self::STATE_NEW);
751
752             if ( ! ($entry instanceof $assoc['targetEntity'])) {
753                 throw new ORMException(
754                     sprintf(
755                         'Found entity of type %s on association %s#%s, but expecting %s',
756                         get_class($entry),
757                         $assoc['sourceEntity'],
758                         $assoc['fieldName'],
759                         $targetClass->name
760                     )
761                 );
762             }
763
764             switch ($state) {
765                 case self::STATE_NEW:
766                     if ( ! $assoc['isCascadePersist']) {
767                         throw ORMInvalidArgumentException::newEntityFoundThroughRelationship($assoc, $entry);
768                     }
769
770                     $this->persistNew($targetClass, $entry);
771                     $this->computeChangeSet($targetClass, $entry);
772                     break;
773
774                 case self::STATE_REMOVED:
775                     // Consume the $value as array (it's either an array or an ArrayAccess)
776                     // and remove the element from Collection.
777                     if ($assoc['type'] & ClassMetadata::TO_MANY) {
778                         unset($value[$key]);
779                     }
780                     break;
781
782                 case self::STATE_DETACHED:
783                     // Can actually not happen right now as we assume STATE_NEW,
784                     // so the exception will be raised from the DBAL layer (constraint violation).
785                     throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
786                     break;
787
788                 default:
789                     // MANAGED associated entities are already taken into account
790                     // during changeset calculation anyway, since they are in the identity map.
791             }
792         }
793     }
794
795     /**
796      * @param ClassMetadata $class
797      * @param object $entity
798      */
799     private function persistNew($class, $entity)
800     {
801         $oid = spl_object_hash($entity);
802
803         if (isset($class->lifecycleCallbacks[Events::prePersist])) {
804             $class->invokeLifecycleCallbacks(Events::prePersist, $entity);
805         }
806
807         if ($this->evm->hasListeners(Events::prePersist)) {
808             $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->em));
809         }
810
811         $idGen = $class->idGenerator;
812
813         if ( ! $idGen->isPostInsertGenerator()) {
814             $idValue = $idGen->generate($this->em, $entity);
815
816             if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
817                 $idValue = array($class->identifier[0] => $idValue);
818
819                 $class->setIdentifierValues($entity, $idValue);
820             }
821
822             $this->entityIdentifiers[$oid] = $idValue;
823         }
824
825         $this->entityStates[$oid] = self::STATE_MANAGED;
826
827         $this->scheduleForInsert($entity);
828     }
829
830     /**
831      * INTERNAL:
832      * Computes the changeset of an individual entity, independently of the
833      * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
834      *
835      * The passed entity must be a managed entity. If the entity already has a change set
836      * because this method is invoked during a commit cycle then the change sets are added.
837      * whereby changes detected in this method prevail.
838      *
839      * @ignore
840      * @param ClassMetadata $class The class descriptor of the entity.
841      * @param object $entity The entity for which to (re)calculate the change set.
842      *
843      * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
844      */
845     public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
846     {
847         $oid = spl_object_hash($entity);
848
849         if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
850             throw ORMInvalidArgumentException::entityNotManaged($entity);
851         }
852
853         // skip if change tracking is "NOTIFY"
854         if ($class->isChangeTrackingNotify()) {
855             return;
856         }
857
858         if ( ! $class->isInheritanceTypeNone()) {
859             $class = $this->em->getClassMetadata(get_class($entity));
860         }
861
862         $actualData = array();
863
864         foreach ($class->reflFields as $name => $refProp) {
865             if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
866                 $actualData[$name] = $refProp->getValue($entity);
867             }
868         }
869
870         $originalData = $this->originalEntityData[$oid];
871         $changeSet = array();
872
873         foreach ($actualData as $propName => $actualValue) {
874             $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
875
876             if (is_object($orgValue) && $orgValue !== $actualValue) {
877                 $changeSet[$propName] = array($orgValue, $actualValue);
878             } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
879                 $changeSet[$propName] = array($orgValue, $actualValue);
880             }
881         }
882
883         if ($changeSet) {
884             if (isset($this->entityChangeSets[$oid])) {
885                 $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
886             }
887
888             $this->originalEntityData[$oid] = $actualData;
889         }
890     }
891
892     /**
893      * Executes all entity insertions for entities of the specified type.
894      *
895      * @param \Doctrine\ORM\Mapping\ClassMetadata $class
896      */
897     private function executeInserts($class)
898     {
899         $className = $class->name;
900         $persister = $this->getEntityPersister($className);
901         $entities  = array();
902
903         $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
904         $hasListeners          = $this->evm->hasListeners(Events::postPersist);
905
906         foreach ($this->entityInsertions as $oid => $entity) {
907             if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
908                 continue;
909             }
910
911             $persister->addInsert($entity);
912
913             unset($this->entityInsertions[$oid]);
914
915             if ($hasLifecycleCallbacks || $hasListeners) {
916                 $entities[] = $entity;
917             }
918         }
919
920         $postInsertIds = $persister->executeInserts();
921
922         if ($postInsertIds) {
923             // Persister returned post-insert IDs
924             foreach ($postInsertIds as $id => $entity) {
925                 $oid     = spl_object_hash($entity);
926                 $idField = $class->identifier[0];
927
928                 $class->reflFields[$idField]->setValue($entity, $id);
929
930                 $this->entityIdentifiers[$oid] = array($idField => $id);
931                 $this->entityStates[$oid] = self::STATE_MANAGED;
932                 $this->originalEntityData[$oid][$idField] = $id;
933
934                 $this->addToIdentityMap($entity);
935             }
936         }
937
938         foreach ($entities as $entity) {
939             if ($hasLifecycleCallbacks) {
940                 $class->invokeLifecycleCallbacks(Events::postPersist, $entity);
941             }
942
943             if ($hasListeners) {
944                 $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em));
945             }
946         }
947     }
948
949     /**
950      * Executes all entity updates for entities of the specified type.
951      *
952      * @param \Doctrine\ORM\Mapping\ClassMetadata $class
953      */
954     private function executeUpdates($class)
955     {
956         $className = $class->name;
957         $persister = $this->getEntityPersister($className);
958
959         $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]);
960         $hasPreUpdateListeners          = $this->evm->hasListeners(Events::preUpdate);
961
962         $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]);
963         $hasPostUpdateListeners          = $this->evm->hasListeners(Events::postUpdate);
964
965         foreach ($this->entityUpdates as $oid => $entity) {
966             if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
967                 continue;
968             }
969
970             if ($hasPreUpdateLifecycleCallbacks) {
971                 $class->invokeLifecycleCallbacks(Events::preUpdate, $entity);
972
973                 $this->recomputeSingleEntityChangeSet($class, $entity);
974             }
975
976             if ($hasPreUpdateListeners) {
977                 $this->evm->dispatchEvent(
978                     Events::preUpdate,
979                     new Event\PreUpdateEventArgs($entity, $this->em, $this->entityChangeSets[$oid])
980                 );
981             }
982
983             if ($this->entityChangeSets[$oid]) {
984                 $persister->update($entity);
985             }
986
987             unset($this->entityUpdates[$oid]);
988
989             if ($hasPostUpdateLifecycleCallbacks) {
990                 $class->invokeLifecycleCallbacks(Events::postUpdate, $entity);
991             }
992
993             if ($hasPostUpdateListeners) {
994                 $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em));
995             }
996         }
997     }
998
999     /**
1000      * Executes all entity deletions for entities of the specified type.
1001      *
1002      * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1003      */
1004     private function executeDeletions($class)
1005     {
1006         $className = $class->name;
1007         $persister = $this->getEntityPersister($className);
1008
1009         $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]);
1010         $hasListeners = $this->evm->hasListeners(Events::postRemove);
1011
1012         foreach ($this->entityDeletions as $oid => $entity) {
1013             if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
1014                 continue;
1015             }
1016
1017             $persister->delete($entity);
1018
1019             unset(
1020                 $this->entityDeletions[$oid],
1021                 $this->entityIdentifiers[$oid],
1022                 $this->originalEntityData[$oid],
1023                 $this->entityStates[$oid]
1024             );
1025
1026             // Entity with this $oid after deletion treated as NEW, even if the $oid
1027             // is obtained by a new entity because the old one went out of scope.
1028             //$this->entityStates[$oid] = self::STATE_NEW;
1029             if ( ! $class->isIdentifierNatural()) {
1030                 $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1031             }
1032
1033             if ($hasLifecycleCallbacks) {
1034                 $class->invokeLifecycleCallbacks(Events::postRemove, $entity);
1035             }
1036
1037             if ($hasListeners) {
1038                 $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em));
1039             }
1040         }
1041     }
1042
1043     /**
1044      * Gets the commit order.
1045      *
1046      * @param array $entityChangeSet
1047      *
1048      * @return array
1049      */
1050     private function getCommitOrder(array $entityChangeSet = null)
1051     {
1052         if ($entityChangeSet === null) {
1053             $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1054         }
1055
1056         $calc = $this->getCommitOrderCalculator();
1057
1058         // See if there are any new classes in the changeset, that are not in the
1059         // commit order graph yet (dont have a node).
1060         // We have to inspect changeSet to be able to correctly build dependencies.
1061         // It is not possible to use IdentityMap here because post inserted ids
1062         // are not yet available.
1063         $newNodes = array();
1064
1065         foreach ($entityChangeSet as $entity) {
1066             $className = $this->em->getClassMetadata(get_class($entity))->name;
1067
1068             if ($calc->hasClass($className)) {
1069                 continue;
1070             }
1071
1072             $class = $this->em->getClassMetadata($className);
1073             $calc->addClass($class);
1074
1075             $newNodes[] = $class;
1076         }
1077
1078         // Calculate dependencies for new nodes
1079         while ($class = array_pop($newNodes)) {
1080             foreach ($class->associationMappings as $assoc) {
1081                 if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1082                     continue;
1083                 }
1084
1085                 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1086
1087                 if ( ! $calc->hasClass($targetClass->name)) {
1088                     $calc->addClass($targetClass);
1089
1090                     $newNodes[] = $targetClass;
1091                 }
1092
1093                 $calc->addDependency($targetClass, $class);
1094
1095                 // If the target class has mapped subclasses, these share the same dependency.
1096                 if ( ! $targetClass->subClasses) {
1097                     continue;
1098                 }
1099
1100                 foreach ($targetClass->subClasses as $subClassName) {
1101                     $targetSubClass = $this->em->getClassMetadata($subClassName);
1102
1103                     if ( ! $calc->hasClass($subClassName)) {
1104                         $calc->addClass($targetSubClass);
1105
1106                         $newNodes[] = $targetSubClass;
1107                     }
1108
1109                     $calc->addDependency($targetSubClass, $class);
1110                 }
1111             }
1112         }
1113
1114         return $calc->getCommitOrder();
1115     }
1116
1117     /**
1118      * Schedules an entity for insertion into the database.
1119      * If the entity already has an identifier, it will be added to the identity map.
1120      *
1121      * @param object $entity The entity to schedule for insertion.
1122      *
1123      * @throws ORMInvalidArgumentException
1124      * @throws \InvalidArgumentException
1125      */
1126     public function scheduleForInsert($entity)
1127     {
1128         $oid = spl_object_hash($entity);
1129
1130         if (isset($this->entityUpdates[$oid])) {
1131             throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1132         }
1133
1134         if (isset($this->entityDeletions[$oid])) {
1135             throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1136         }
1137         if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1138             throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1139         }
1140
1141         if (isset($this->entityInsertions[$oid])) {
1142             throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1143         }
1144
1145         $this->entityInsertions[$oid] = $entity;
1146
1147         if (isset($this->entityIdentifiers[$oid])) {
1148             $this->addToIdentityMap($entity);
1149         }
1150
1151         if ($entity instanceof NotifyPropertyChanged) {
1152             $entity->addPropertyChangedListener($this);
1153         }
1154     }
1155
1156     /**
1157      * Checks whether an entity is scheduled for insertion.
1158      *
1159      * @param object $entity
1160      *
1161      * @return boolean
1162      */
1163     public function isScheduledForInsert($entity)
1164     {
1165         return isset($this->entityInsertions[spl_object_hash($entity)]);
1166     }
1167
1168     /**
1169      * Schedules an entity for being updated.
1170      *
1171      * @param object $entity The entity to schedule for being updated.
1172      *
1173      * @throws ORMInvalidArgumentException
1174      */
1175     public function scheduleForUpdate($entity)
1176     {
1177         $oid = spl_object_hash($entity);
1178
1179         if ( ! isset($this->entityIdentifiers[$oid])) {
1180             throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1181         }
1182
1183         if (isset($this->entityDeletions[$oid])) {
1184             throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1185         }
1186
1187         if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1188             $this->entityUpdates[$oid] = $entity;
1189         }
1190     }
1191
1192     /**
1193      * INTERNAL:
1194      * Schedules an extra update that will be executed immediately after the
1195      * regular entity updates within the currently running commit cycle.
1196      *
1197      * Extra updates for entities are stored as (entity, changeset) tuples.
1198      *
1199      * @ignore
1200      * @param object $entity The entity for which to schedule an extra update.
1201      * @param array $changeset The changeset of the entity (what to update).
1202      */
1203     public function scheduleExtraUpdate($entity, array $changeset)
1204     {
1205         $oid         = spl_object_hash($entity);
1206         $extraUpdate = array($entity, $changeset);
1207
1208         if (isset($this->extraUpdates[$oid])) {
1209             list($ignored, $changeset2) = $this->extraUpdates[$oid];
1210
1211             $extraUpdate = array($entity, $changeset + $changeset2);
1212         }
1213
1214         $this->extraUpdates[$oid] = $extraUpdate;
1215     }
1216
1217     /**
1218      * Checks whether an entity is registered as dirty in the unit of work.
1219      * Note: Is not very useful currently as dirty entities are only registered
1220      * at commit time.
1221      *
1222      * @param object $entity
1223      *
1224      * @return boolean
1225      */
1226     public function isScheduledForUpdate($entity)
1227     {
1228         return isset($this->entityUpdates[spl_object_hash($entity)]);
1229     }
1230
1231
1232     /**
1233      * Checks whether an entity is registered to be checked in the unit of work.
1234      *
1235      * @param object $entity
1236      *
1237      * @return boolean
1238      */
1239     public function isScheduledForDirtyCheck($entity)
1240     {
1241         $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
1242
1243         return isset($this->scheduledForDirtyCheck[$rootEntityName][spl_object_hash($entity)]);
1244     }
1245
1246     /**
1247      * INTERNAL:
1248      * Schedules an entity for deletion.
1249      *
1250      * @param object $entity
1251      */
1252     public function scheduleForDelete($entity)
1253     {
1254         $oid = spl_object_hash($entity);
1255
1256         if (isset($this->entityInsertions[$oid])) {
1257             if ($this->isInIdentityMap($entity)) {
1258                 $this->removeFromIdentityMap($entity);
1259             }
1260
1261             unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1262
1263             return; // entity has not been persisted yet, so nothing more to do.
1264         }
1265
1266         if ( ! $this->isInIdentityMap($entity)) {
1267             return;
1268         }
1269
1270         $this->removeFromIdentityMap($entity);
1271
1272         if (isset($this->entityUpdates[$oid])) {
1273             unset($this->entityUpdates[$oid]);
1274         }
1275
1276         if ( ! isset($this->entityDeletions[$oid])) {
1277             $this->entityDeletions[$oid] = $entity;
1278             $this->entityStates[$oid]    = self::STATE_REMOVED;
1279         }
1280     }
1281
1282     /**
1283      * Checks whether an entity is registered as removed/deleted with the unit
1284      * of work.
1285      *
1286      * @param object $entity
1287      *
1288      * @return boolean
1289      */
1290     public function isScheduledForDelete($entity)
1291     {
1292         return isset($this->entityDeletions[spl_object_hash($entity)]);
1293     }
1294
1295     /**
1296      * Checks whether an entity is scheduled for insertion, update or deletion.
1297      *
1298      * @param $entity
1299      *
1300      * @return boolean
1301      */
1302     public function isEntityScheduled($entity)
1303     {
1304         $oid = spl_object_hash($entity);
1305
1306         return isset($this->entityInsertions[$oid])
1307             || isset($this->entityUpdates[$oid])
1308             || isset($this->entityDeletions[$oid]);
1309     }
1310
1311     /**
1312      * INTERNAL:
1313      * Registers an entity in the identity map.
1314      * Note that entities in a hierarchy are registered with the class name of
1315      * the root entity.
1316      *
1317      * @ignore
1318      * @param object $entity  The entity to register.
1319      *
1320      * @throws ORMInvalidArgumentException
1321      *
1322      * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1323      *                  the entity in question is already managed.
1324      */
1325     public function addToIdentityMap($entity)
1326     {
1327         $classMetadata = $this->em->getClassMetadata(get_class($entity));
1328         $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
1329
1330         if ($idHash === '') {
1331             throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
1332         }
1333
1334         $className = $classMetadata->rootEntityName;
1335
1336         if (isset($this->identityMap[$className][$idHash])) {
1337             return false;
1338         }
1339
1340         $this->identityMap[$className][$idHash] = $entity;
1341
1342         return true;
1343     }
1344
1345     /**
1346      * Gets the state of an entity with regard to the current unit of work.
1347      *
1348      * @param object $entity
1349      * @param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1350      *                        This parameter can be set to improve performance of entity state detection
1351      *                        by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1352      *                        is either known or does not matter for the caller of the method.
1353      *
1354      * @return int The entity state.
1355      */
1356     public function getEntityState($entity, $assume = null)
1357     {
1358         $oid = spl_object_hash($entity);
1359
1360         if (isset($this->entityStates[$oid])) {
1361             return $this->entityStates[$oid];
1362         }
1363
1364         if ($assume !== null) {
1365             return $assume;
1366         }
1367
1368         // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1369         // Note that you can not remember the NEW or DETACHED state in _entityStates since
1370         // the UoW does not hold references to such objects and the object hash can be reused.
1371         // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1372         $class = $this->em->getClassMetadata(get_class($entity));
1373         $id    = $class->getIdentifierValues($entity);
1374
1375         if ( ! $id) {
1376             return self::STATE_NEW;
1377         }
1378
1379         switch (true) {
1380             case ($class->isIdentifierNatural());
1381                 // Check for a version field, if available, to avoid a db lookup.
1382                 if ($class->isVersioned) {
1383                     return ($class->getFieldValue($entity, $class->versionField))
1384                         ? self::STATE_DETACHED
1385                         : self::STATE_NEW;
1386                 }
1387
1388                 // Last try before db lookup: check the identity map.
1389                 if ($this->tryGetById($id, $class->rootEntityName)) {
1390                     return self::STATE_DETACHED;
1391                 }
1392
1393                 // db lookup
1394                 if ($this->getEntityPersister($class->name)->exists($entity)) {
1395                     return self::STATE_DETACHED;
1396                 }
1397
1398                 return self::STATE_NEW;
1399
1400             case ( ! $class->idGenerator->isPostInsertGenerator()):
1401                 // if we have a pre insert generator we can't be sure that having an id
1402                 // really means that the entity exists. We have to verify this through
1403                 // the last resort: a db lookup
1404
1405                 // Last try before db lookup: check the identity map.
1406                 if ($this->tryGetById($id, $class->rootEntityName)) {
1407                     return self::STATE_DETACHED;
1408                 }
1409
1410                 // db lookup
1411                 if ($this->getEntityPersister($class->name)->exists($entity)) {
1412                     return self::STATE_DETACHED;
1413                 }
1414
1415                 return self::STATE_NEW;
1416
1417             default:
1418                 return self::STATE_DETACHED;
1419         }
1420     }
1421
1422     /**
1423      * INTERNAL:
1424      * Removes an entity from the identity map. This effectively detaches the
1425      * entity from the persistence management of Doctrine.
1426      *
1427      * @ignore
1428      * @param object $entity
1429      *
1430      * @throws ORMInvalidArgumentException
1431      *
1432      * @return boolean
1433      */
1434     public function removeFromIdentityMap($entity)
1435     {
1436         $oid           = spl_object_hash($entity);
1437         $classMetadata = $this->em->getClassMetadata(get_class($entity));
1438         $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1439
1440         if ($idHash === '') {
1441             throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1442         }
1443
1444         $className = $classMetadata->rootEntityName;
1445
1446         if (isset($this->identityMap[$className][$idHash])) {
1447             unset($this->identityMap[$className][$idHash]);
1448             unset($this->readOnlyObjects[$oid]);
1449
1450             //$this->entityStates[$oid] = self::STATE_DETACHED;
1451
1452             return true;
1453         }
1454
1455         return false;
1456     }
1457
1458     /**
1459      * INTERNAL:
1460      * Gets an entity in the identity map by its identifier hash.
1461      *
1462      * @ignore
1463      * @param string $idHash
1464      * @param string $rootClassName
1465      *
1466      * @return object
1467      */
1468     public function getByIdHash($idHash, $rootClassName)
1469     {
1470         return $this->identityMap[$rootClassName][$idHash];
1471     }
1472
1473     /**
1474      * INTERNAL:
1475      * Tries to get an entity by its identifier hash. If no entity is found for
1476      * the given hash, FALSE is returned.
1477      *
1478      * @ignore
1479      * @param string $idHash
1480      * @param string $rootClassName
1481      *
1482      * @return mixed The found entity or FALSE.
1483      */
1484     public function tryGetByIdHash($idHash, $rootClassName)
1485     {
1486         if (isset($this->identityMap[$rootClassName][$idHash])) {
1487             return $this->identityMap[$rootClassName][$idHash];
1488         }
1489
1490         return false;
1491     }
1492
1493     /**
1494      * Checks whether an entity is registered in the identity map of this UnitOfWork.
1495      *
1496      * @param object $entity
1497      *
1498      * @return boolean
1499      */
1500     public function isInIdentityMap($entity)
1501     {
1502         $oid = spl_object_hash($entity);
1503
1504         if ( ! isset($this->entityIdentifiers[$oid])) {
1505             return false;
1506         }
1507
1508         $classMetadata = $this->em->getClassMetadata(get_class($entity));
1509         $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1510
1511         if ($idHash === '') {
1512             return false;
1513         }
1514
1515         return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
1516     }
1517
1518     /**
1519      * INTERNAL:
1520      * Checks whether an identifier hash exists in the identity map.
1521      *
1522      * @ignore
1523      * @param string $idHash
1524      * @param string $rootClassName
1525      *
1526      * @return boolean
1527      */
1528     public function containsIdHash($idHash, $rootClassName)
1529     {
1530         return isset($this->identityMap[$rootClassName][$idHash]);
1531     }
1532
1533     /**
1534      * Persists an entity as part of the current unit of work.
1535      *
1536      * @param object $entity The entity to persist.
1537      */
1538     public function persist($entity)
1539     {
1540         $visited = array();
1541
1542         $this->doPersist($entity, $visited);
1543     }
1544
1545     /**
1546      * Persists an entity as part of the current unit of work.
1547      *
1548      * This method is internally called during persist() cascades as it tracks
1549      * the already visited entities to prevent infinite recursions.
1550      *
1551      * @param object $entity The entity to persist.
1552      * @param array $visited The already visited entities.
1553      *
1554      * @throws ORMInvalidArgumentException
1555      * @throws UnexpectedValueException
1556      */
1557     private function doPersist($entity, array &$visited)
1558     {
1559         $oid = spl_object_hash($entity);
1560
1561         if (isset($visited[$oid])) {
1562             return; // Prevent infinite recursion
1563         }
1564
1565         $visited[$oid] = $entity; // Mark visited
1566
1567         $class = $this->em->getClassMetadata(get_class($entity));
1568
1569         // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1570         // If we would detect DETACHED here we would throw an exception anyway with the same
1571         // consequences (not recoverable/programming error), so just assuming NEW here
1572         // lets us avoid some database lookups for entities with natural identifiers.
1573         $entityState = $this->getEntityState($entity, self::STATE_NEW);
1574
1575         switch ($entityState) {
1576             case self::STATE_MANAGED:
1577                 // Nothing to do, except if policy is "deferred explicit"
1578                 if ($class->isChangeTrackingDeferredExplicit()) {
1579                     $this->scheduleForDirtyCheck($entity);
1580                 }
1581                 break;
1582
1583             case self::STATE_NEW:
1584                 $this->persistNew($class, $entity);
1585                 break;
1586
1587             case self::STATE_REMOVED:
1588                 // Entity becomes managed again
1589                 unset($this->entityDeletions[$oid]);
1590
1591                 $this->entityStates[$oid] = self::STATE_MANAGED;
1592                 break;
1593
1594             case self::STATE_DETACHED:
1595                 // Can actually not happen right now since we assume STATE_NEW.
1596                 throw ORMInvalidArgumentException::detachedEntityCannot($entity, "persisted");
1597
1598             default:
1599                 throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1600         }
1601
1602         $this->cascadePersist($entity, $visited);
1603     }
1604
1605     /**
1606      * Deletes an entity as part of the current unit of work.
1607      *
1608      * @param object $entity The entity to remove.
1609      */
1610     public function remove($entity)
1611     {
1612         $visited = array();
1613
1614         $this->doRemove($entity, $visited);
1615     }
1616
1617     /**
1618      * Deletes an entity as part of the current unit of work.
1619      *
1620      * This method is internally called during delete() cascades as it tracks
1621      * the already visited entities to prevent infinite recursions.
1622      *
1623      * @param object $entity The entity to delete.
1624      * @param array $visited The map of the already visited entities.
1625      *
1626      * @throws ORMInvalidArgumentException If the instance is a detached entity.
1627      * @throws UnexpectedValueException
1628      */
1629     private function doRemove($entity, array &$visited)
1630     {
1631         $oid = spl_object_hash($entity);
1632
1633         if (isset($visited[$oid])) {
1634             return; // Prevent infinite recursion
1635         }
1636
1637         $visited[$oid] = $entity; // mark visited
1638
1639         // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1640         // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1641         $this->cascadeRemove($entity, $visited);
1642
1643         $class       = $this->em->getClassMetadata(get_class($entity));
1644         $entityState = $this->getEntityState($entity);
1645
1646         switch ($entityState) {
1647             case self::STATE_NEW:
1648             case self::STATE_REMOVED:
1649                 // nothing to do
1650                 break;
1651
1652             case self::STATE_MANAGED:
1653                 if (isset($class->lifecycleCallbacks[Events::preRemove])) {
1654                     $class->invokeLifecycleCallbacks(Events::preRemove, $entity);
1655                 }
1656
1657                 if ($this->evm->hasListeners(Events::preRemove)) {
1658                     $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity, $this->em));
1659                 }
1660
1661                 $this->scheduleForDelete($entity);
1662                 break;
1663
1664             case self::STATE_DETACHED:
1665                 throw ORMInvalidArgumentException::detachedEntityCannot($entity, "removed");
1666             default:
1667                 throw new UnexpectedValueException("Unexpected entity state: $entityState." . self::objToStr($entity));
1668         }
1669
1670     }
1671
1672     /**
1673      * Merges the state of the given detached entity into this UnitOfWork.
1674      *
1675      * @param object $entity
1676      *
1677      * @throws OptimisticLockException If the entity uses optimistic locking through a version
1678      *         attribute and the version check against the managed copy fails.
1679      *
1680      * @return object The managed copy of the entity.
1681      *
1682      * @todo Require active transaction!? OptimisticLockException may result in undefined state!?
1683      */
1684     public function merge($entity)
1685     {
1686         $visited = array();
1687
1688         return $this->doMerge($entity, $visited);
1689     }
1690
1691     /**
1692      * Executes a merge operation on an entity.
1693      *
1694      * @param object $entity
1695      * @param array $visited
1696      * @param object $prevManagedCopy
1697      * @param array $assoc
1698      *
1699      * @throws OptimisticLockException If the entity uses optimistic locking through a version
1700      *         attribute and the version check against the managed copy fails.
1701      * @throws ORMInvalidArgumentException If the entity instance is NEW.
1702      * @throws EntityNotFoundException
1703      *
1704      * @return object The managed copy of the entity.
1705      */
1706     private function doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
1707     {
1708         $oid = spl_object_hash($entity);
1709
1710         if (isset($visited[$oid])) {
1711             return $visited[$oid]; // Prevent infinite recursion
1712         }
1713
1714         $visited[$oid] = $entity; // mark visited
1715
1716         $class = $this->em->getClassMetadata(get_class($entity));
1717
1718         // First we assume DETACHED, although it can still be NEW but we can avoid
1719         // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1720         // we need to fetch it from the db anyway in order to merge.
1721         // MANAGED entities are ignored by the merge operation.
1722         $managedCopy = $entity;
1723
1724         if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1725             if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
1726                 $entity->__load();
1727             }
1728
1729             // Try to look the entity up in the identity map.
1730             $id = $class->getIdentifierValues($entity);
1731
1732             // If there is no ID, it is actually NEW.
1733             if ( ! $id) {
1734                 $managedCopy = $this->newInstance($class);
1735
1736                 $this->persistNew($class, $managedCopy);
1737             } else {
1738                 $flatId = $id;
1739                 if ($class->containsForeignIdentifier) {
1740                     // convert foreign identifiers into scalar foreign key
1741                     // values to avoid object to string conversion failures.
1742                     foreach ($id as $idField => $idValue) {
1743                         if (isset($class->associationMappings[$idField])) {
1744                             $targetClassMetadata = $this->em->getClassMetadata($class->associationMappings[$idField]['targetEntity']);
1745                             $associatedId        = $this->getEntityIdentifier($idValue);
1746
1747                             $flatId[$idField] = $associatedId[$targetClassMetadata->identifier[0]];
1748                         }
1749                     }
1750                 }
1751
1752                 $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
1753
1754                 if ($managedCopy) {
1755                     // We have the entity in-memory already, just make sure its not removed.
1756                     if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
1757                         throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, "merge");
1758                     }
1759                 } else {
1760                     // We need to fetch the managed copy in order to merge.
1761                     $managedCopy = $this->em->find($class->name, $flatId);
1762                 }
1763
1764                 if ($managedCopy === null) {
1765                     // If the identifier is ASSIGNED, it is NEW, otherwise an error
1766                     // since the managed entity was not found.
1767                     if ( ! $class->isIdentifierNatural()) {
1768                         throw new EntityNotFoundException;
1769                     }
1770
1771                     $managedCopy = $this->newInstance($class);
1772                     $class->setIdentifierValues($managedCopy, $id);
1773
1774                     $this->persistNew($class, $managedCopy);
1775                 } else {
1776                     if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
1777                         $managedCopy->__load();
1778                     }
1779                 }
1780             }
1781
1782             if ($class->isVersioned) {
1783                 $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1784                 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
1785
1786                 // Throw exception if versions dont match.
1787                 if ($managedCopyVersion != $entityVersion) {
1788                     throw OptimisticLockException::lockFailedVersionMissmatch($entity, $entityVersion, $managedCopyVersion);
1789                 }
1790             }
1791
1792             // Merge state of $entity into existing (managed) entity
1793             foreach ($class->reflClass->getProperties() as $prop) {
1794                 $name = $prop->name;
1795                 $prop->setAccessible(true);
1796                 if ( ! isset($class->associationMappings[$name])) {
1797                     if ( ! $class->isIdentifier($name)) {
1798                         $prop->setValue($managedCopy, $prop->getValue($entity));
1799                     }
1800                 } else {
1801                     $assoc2 = $class->associationMappings[$name];
1802                     if ($assoc2['type'] & ClassMetadata::TO_ONE) {
1803                         $other = $prop->getValue($entity);
1804                         if ($other === null) {
1805                             $prop->setValue($managedCopy, null);
1806                         } else if ($other instanceof Proxy && !$other->__isInitialized__) {
1807                             // do not merge fields marked lazy that have not been fetched.
1808                             continue;
1809                         } else if ( ! $assoc2['isCascadeMerge']) {
1810                             if ($this->getEntityState($other, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1811                                 $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
1812                                 $relatedId = $targetClass->getIdentifierValues($other);
1813
1814                                 if ($targetClass->subClasses) {
1815                                     $other = $this->em->find($targetClass->name, $relatedId);
1816                                 } else {
1817                                     $other = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $relatedId);
1818                                     $this->registerManaged($other, $relatedId, array());
1819                                 }
1820                             }
1821                             $prop->setValue($managedCopy, $other);
1822                         }
1823                     } else {
1824                         $mergeCol = $prop->getValue($entity);
1825                         if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
1826                             // do not merge fields marked lazy that have not been fetched.
1827                             // keep the lazy persistent collection of the managed copy.
1828                             continue;
1829                         }
1830
1831                         $managedCol = $prop->getValue($managedCopy);
1832                         if (!$managedCol) {
1833                             $managedCol = new PersistentCollection($this->em,
1834                                     $this->em->getClassMetadata($assoc2['targetEntity']),
1835                                     new ArrayCollection
1836                                     );
1837                             $managedCol->setOwner($managedCopy, $assoc2);
1838                             $prop->setValue($managedCopy, $managedCol);
1839                             $this->originalEntityData[$oid][$name] = $managedCol;
1840                         }
1841                         if ($assoc2['isCascadeMerge']) {
1842                             $managedCol->initialize();
1843
1844                             // clear and set dirty a managed collection if its not also the same collection to merge from.
1845                             if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
1846                                 $managedCol->unwrap()->clear();
1847                                 $managedCol->setDirty(true);
1848
1849                                 if ($assoc2['isOwningSide'] && $assoc2['type'] == ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) {
1850                                     $this->scheduleForDirtyCheck($managedCopy);
1851                                 }
1852                             }
1853                         }
1854                     }
1855                 }
1856
1857                 if ($class->isChangeTrackingNotify()) {
1858                     // Just treat all properties as changed, there is no other choice.
1859                     $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1860                 }
1861             }
1862
1863             if ($class->isChangeTrackingDeferredExplicit()) {
1864                 $this->scheduleForDirtyCheck($entity);
1865             }
1866         }
1867
1868         if ($prevManagedCopy !== null) {
1869             $assocField = $assoc['fieldName'];
1870             $prevClass = $this->em->getClassMetadata(get_class($prevManagedCopy));
1871
1872             if ($assoc['type'] & ClassMetadata::TO_ONE) {
1873                 $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1874             } else {
1875                 $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1876
1877                 if ($assoc['type'] == ClassMetadata::ONE_TO_MANY) {
1878                     $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1879                 }
1880             }
1881         }
1882
1883         // Mark the managed copy visited as well
1884         $visited[spl_object_hash($managedCopy)] = true;
1885
1886         $this->cascadeMerge($entity, $managedCopy, $visited);
1887
1888         return $managedCopy;
1889     }
1890
1891     /**
1892      * Detaches an entity from the persistence management. It's persistence will
1893      * no longer be managed by Doctrine.
1894      *
1895      * @param object $entity The entity to detach.
1896      */
1897     public function detach($entity)
1898     {
1899         $visited = array();
1900
1901         $this->doDetach($entity, $visited);
1902     }
1903
1904     /**
1905      * Executes a detach operation on the given entity.
1906      *
1907      * @param object $entity
1908      * @param array $visited
1909      * @param boolean $noCascade if true, don't cascade detach operation
1910      */
1911     private function doDetach($entity, array &$visited, $noCascade = false)
1912     {
1913         $oid = spl_object_hash($entity);
1914
1915         if (isset($visited[$oid])) {
1916             return; // Prevent infinite recursion
1917         }
1918
1919         $visited[$oid] = $entity; // mark visited
1920
1921         switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1922             case self::STATE_MANAGED:
1923                 if ($this->isInIdentityMap($entity)) {
1924                     $this->removeFromIdentityMap($entity);
1925                 }
1926
1927                 unset(
1928                     $this->entityInsertions[$oid],
1929                     $this->entityUpdates[$oid],
1930                     $this->entityDeletions[$oid],
1931                     $this->entityIdentifiers[$oid],
1932                     $this->entityStates[$oid],
1933                     $this->originalEntityData[$oid]
1934                 );
1935                 break;
1936             case self::STATE_NEW:
1937             case self::STATE_DETACHED:
1938                 return;
1939         }
1940
1941         if ( ! $noCascade) {
1942             $this->cascadeDetach($entity, $visited);
1943         }
1944     }
1945
1946     /**
1947      * Refreshes the state of the given entity from the database, overwriting
1948      * any local, unpersisted changes.
1949      *
1950      * @param object $entity The entity to refresh.
1951      *
1952      * @throws InvalidArgumentException If the entity is not MANAGED.
1953      */
1954     public function refresh($entity)
1955     {
1956         $visited = array();
1957
1958         $this->doRefresh($entity, $visited);
1959     }
1960
1961     /**
1962      * Executes a refresh operation on an entity.
1963      *
1964      * @param object $entity The entity to refresh.
1965      * @param array $visited The already visited entities during cascades.
1966      *
1967      * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1968      */
1969     private function doRefresh($entity, array &$visited)
1970     {
1971         $oid = spl_object_hash($entity);
1972
1973         if (isset($visited[$oid])) {
1974             return; // Prevent infinite recursion
1975         }
1976
1977         $visited[$oid] = $entity; // mark visited
1978
1979         $class = $this->em->getClassMetadata(get_class($entity));
1980
1981         if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1982             throw ORMInvalidArgumentException::entityNotManaged($entity);
1983         }
1984
1985         $this->getEntityPersister($class->name)->refresh(
1986             array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1987             $entity
1988         );
1989
1990         $this->cascadeRefresh($entity, $visited);
1991     }
1992
1993     /**
1994      * Cascades a refresh operation to associated entities.
1995      *
1996      * @param object $entity
1997      * @param array $visited
1998      */
1999     private function cascadeRefresh($entity, array &$visited)
2000     {
2001         $class = $this->em->getClassMetadata(get_class($entity));
2002
2003         $associationMappings = array_filter(
2004             $class->associationMappings,
2005             function ($assoc) { return $assoc['isCascadeRefresh']; }
2006         );
2007
2008         foreach ($associationMappings as $assoc) {
2009             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2010
2011             switch (true) {
2012                 case ($relatedEntities instanceof PersistentCollection):
2013                     // Unwrap so that foreach() does not initialize
2014                     $relatedEntities = $relatedEntities->unwrap();
2015                     // break; is commented intentionally!
2016
2017                 case ($relatedEntities instanceof Collection):
2018                 case (is_array($relatedEntities)):
2019                     foreach ($relatedEntities as $relatedEntity) {
2020                         $this->doRefresh($relatedEntity, $visited);
2021                     }
2022                     break;
2023
2024                 case ($relatedEntities !== null):
2025                     $this->doRefresh($relatedEntities, $visited);
2026                     break;
2027
2028                 default:
2029                     // Do nothing
2030             }
2031         }
2032     }
2033
2034     /**
2035      * Cascades a detach operation to associated entities.
2036      *
2037      * @param object $entity
2038      * @param array $visited
2039      */
2040     private function cascadeDetach($entity, array &$visited)
2041     {
2042         $class = $this->em->getClassMetadata(get_class($entity));
2043
2044         $associationMappings = array_filter(
2045             $class->associationMappings,
2046             function ($assoc) { return $assoc['isCascadeDetach']; }
2047         );
2048
2049         foreach ($associationMappings as $assoc) {
2050             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2051
2052             switch (true) {
2053                 case ($relatedEntities instanceof PersistentCollection):
2054                     // Unwrap so that foreach() does not initialize
2055                     $relatedEntities = $relatedEntities->unwrap();
2056                     // break; is commented intentionally!
2057
2058                 case ($relatedEntities instanceof Collection):
2059                 case (is_array($relatedEntities)):
2060                     foreach ($relatedEntities as $relatedEntity) {
2061                         $this->doDetach($relatedEntity, $visited);
2062                     }
2063                     break;
2064
2065                 case ($relatedEntities !== null):
2066                     $this->doDetach($relatedEntities, $visited);
2067                     break;
2068
2069                 default:
2070                     // Do nothing
2071             }
2072         }
2073     }
2074
2075     /**
2076      * Cascades a merge operation to associated entities.
2077      *
2078      * @param object $entity
2079      * @param object $managedCopy
2080      * @param array $visited
2081      */
2082     private function cascadeMerge($entity, $managedCopy, array &$visited)
2083     {
2084         $class = $this->em->getClassMetadata(get_class($entity));
2085
2086         $associationMappings = array_filter(
2087             $class->associationMappings,
2088             function ($assoc) { return $assoc['isCascadeMerge']; }
2089         );
2090
2091         foreach ($associationMappings as $assoc) {
2092             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2093
2094             if ($relatedEntities instanceof Collection) {
2095                 if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2096                     continue;
2097                 }
2098
2099                 if ($relatedEntities instanceof PersistentCollection) {
2100                     // Unwrap so that foreach() does not initialize
2101                     $relatedEntities = $relatedEntities->unwrap();
2102                 }
2103
2104                 foreach ($relatedEntities as $relatedEntity) {
2105                     $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
2106                 }
2107             } else if ($relatedEntities !== null) {
2108                 $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
2109             }
2110         }
2111     }
2112
2113     /**
2114      * Cascades the save operation to associated entities.
2115      *
2116      * @param object $entity
2117      * @param array $visited
2118      *
2119      * @return void
2120      */
2121     private function cascadePersist($entity, array &$visited)
2122     {
2123         $class = $this->em->getClassMetadata(get_class($entity));
2124
2125         $associationMappings = array_filter(
2126             $class->associationMappings,
2127             function ($assoc) { return $assoc['isCascadePersist']; }
2128         );
2129
2130         foreach ($associationMappings as $assoc) {
2131             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2132
2133             switch (true) {
2134                 case ($relatedEntities instanceof PersistentCollection):
2135                     // Unwrap so that foreach() does not initialize
2136                     $relatedEntities = $relatedEntities->unwrap();
2137                     // break; is commented intentionally!
2138
2139                 case ($relatedEntities instanceof Collection):
2140                 case (is_array($relatedEntities)):
2141                     foreach ($relatedEntities as $relatedEntity) {
2142                         $this->doPersist($relatedEntity, $visited);
2143                     }
2144                     break;
2145
2146                 case ($relatedEntities !== null):
2147                     $this->doPersist($relatedEntities, $visited);
2148                     break;
2149
2150                 default:
2151                     // Do nothing
2152             }
2153         }
2154     }
2155
2156     /**
2157      * Cascades the delete operation to associated entities.
2158      *
2159      * @param object $entity
2160      * @param array $visited
2161      */
2162     private function cascadeRemove($entity, array &$visited)
2163     {
2164         $class = $this->em->getClassMetadata(get_class($entity));
2165
2166         $associationMappings = array_filter(
2167             $class->associationMappings,
2168             function ($assoc) { return $assoc['isCascadeRemove']; }
2169         );
2170
2171         foreach ($associationMappings as $assoc) {
2172             if ($entity instanceof Proxy && !$entity->__isInitialized__) {
2173                 $entity->__load();
2174             }
2175
2176             $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
2177
2178             switch (true) {
2179                 case ($relatedEntities instanceof Collection):
2180                 case (is_array($relatedEntities)):
2181                     // If its a PersistentCollection initialization is intended! No unwrap!
2182                     foreach ($relatedEntities as $relatedEntity) {
2183                         $this->doRemove($relatedEntity, $visited);
2184                     }
2185                     break;
2186
2187                 case ($relatedEntities !== null):
2188                     $this->doRemove($relatedEntities, $visited);
2189                     break;
2190
2191                 default:
2192                     // Do nothing
2193             }
2194         }
2195     }
2196
2197     /**
2198      * Acquire a lock on the given entity.
2199      *
2200      * @param object $entity
2201      * @param int $lockMode
2202      * @param int $lockVersion
2203      *
2204      * @throws ORMInvalidArgumentException
2205      * @throws TransactionRequiredException
2206      * @throws OptimisticLockException
2207      *
2208      * @return void
2209      */
2210     public function lock($entity, $lockMode, $lockVersion = null)
2211     {
2212         if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) {
2213             throw ORMInvalidArgumentException::entityNotManaged($entity);
2214         }
2215
2216         $class = $this->em->getClassMetadata(get_class($entity));
2217
2218         switch ($lockMode) {
2219             case \Doctrine\DBAL\LockMode::OPTIMISTIC;
2220                 if ( ! $class->isVersioned) {
2221                     throw OptimisticLockException::notVersioned($class->name);
2222                 }
2223
2224                 if ($lockVersion === null) {
2225                     return;
2226                 }
2227
2228                 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
2229
2230                 if ($entityVersion != $lockVersion) {
2231                     throw OptimisticLockException::lockFailedVersionMissmatch($entity, $lockVersion, $entityVersion);
2232                 }
2233
2234                 break;
2235
2236             case \Doctrine\DBAL\LockMode::PESSIMISTIC_READ:
2237             case \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE:
2238                 if (!$this->em->getConnection()->isTransactionActive()) {
2239                     throw TransactionRequiredException::transactionRequired();
2240                 }
2241
2242                 $oid = spl_object_hash($entity);
2243
2244                 $this->getEntityPersister($class->name)->lock(
2245                     array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
2246                     $lockMode
2247                 );
2248                 break;
2249
2250             default:
2251                 // Do nothing
2252         }
2253     }
2254
2255     /**
2256      * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
2257      *
2258      * @return \Doctrine\ORM\Internal\CommitOrderCalculator
2259      */
2260     public function getCommitOrderCalculator()
2261     {
2262         if ($this->commitOrderCalculator === null) {
2263             $this->commitOrderCalculator = new Internal\CommitOrderCalculator;
2264         }
2265
2266         return $this->commitOrderCalculator;
2267     }
2268
2269     /**
2270      * Clears the UnitOfWork.
2271      *
2272      * @param string $entityName if given, only entities of this type will get detached
2273      */
2274     public function clear($entityName = null)
2275     {
2276         if ($entityName === null) {
2277             $this->identityMap =
2278             $this->entityIdentifiers =
2279             $this->originalEntityData =
2280             $this->entityChangeSets =
2281             $this->entityStates =
2282             $this->scheduledForDirtyCheck =
2283             $this->entityInsertions =
2284             $this->entityUpdates =
2285             $this->entityDeletions =
2286             $this->collectionDeletions =
2287             $this->collectionUpdates =
2288             $this->extraUpdates =
2289             $this->readOnlyObjects =
2290             $this->orphanRemovals = array();
2291
2292             if ($this->commitOrderCalculator !== null) {
2293                 $this->commitOrderCalculator->clear();
2294             }
2295         } else {
2296             $visited = array();
2297             foreach ($this->identityMap as $className => $entities) {
2298                 if ($className === $entityName) {
2299                     foreach ($entities as $entity) {
2300                         $this->doDetach($entity, $visited, true);
2301                     }
2302                 }
2303             }
2304         }
2305
2306         if ($this->evm->hasListeners(Events::onClear)) {
2307             $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
2308         }
2309     }
2310
2311     /**
2312      * INTERNAL:
2313      * Schedules an orphaned entity for removal. The remove() operation will be
2314      * invoked on that entity at the beginning of the next commit of this
2315      * UnitOfWork.
2316      *
2317      * @ignore
2318      * @param object $entity
2319      */
2320     public function scheduleOrphanRemoval($entity)
2321     {
2322         $this->orphanRemovals[spl_object_hash($entity)] = $entity;
2323     }
2324
2325     /**
2326      * INTERNAL:
2327      * Schedules a complete collection for removal when this UnitOfWork commits.
2328      *
2329      * @param PersistentCollection $coll
2330      */
2331     public function scheduleCollectionDeletion(PersistentCollection $coll)
2332     {
2333         $coid = spl_object_hash($coll);
2334
2335         //TODO: if $coll is already scheduled for recreation ... what to do?
2336         // Just remove $coll from the scheduled recreations?
2337         if (isset($this->collectionUpdates[$coid])) {
2338             unset($this->collectionUpdates[$coid]);
2339         }
2340
2341         $this->collectionDeletions[$coid] = $coll;
2342     }
2343
2344     /**
2345      * @param PersistentCollection $coll
2346      *
2347      * @return bool
2348      */
2349     public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2350     {
2351         return isset($this->collectionDeletions[spl_object_hash($coll)]);
2352     }
2353
2354     /**
2355      * @param ClassMetadata $class
2356      *
2357      * @return \Doctrine\Common\Persistence\ObjectManagerAware|object
2358      */
2359     private function newInstance($class)
2360     {
2361         $entity = $class->newInstance();
2362
2363         if ($entity instanceof \Doctrine\Common\Persistence\ObjectManagerAware) {
2364             $entity->injectObjectManager($this->em, $class);
2365         }
2366
2367         return $entity;
2368     }
2369
2370     /**
2371      * INTERNAL:
2372      * Creates an entity. Used for reconstitution of persistent entities.
2373      *
2374      * @ignore
2375      * @param string $className The name of the entity class.
2376      * @param array $data The data for the entity.
2377      * @param array $hints Any hints to account for during reconstitution/lookup of the entity.
2378      *
2379      * @return object The managed entity instance.
2380      * @internal Highly performance-sensitive method.
2381      *
2382      * @todo Rename: getOrCreateEntity
2383      */
2384     public function createEntity($className, array $data, &$hints = array())
2385     {
2386         $class = $this->em->getClassMetadata($className);
2387         //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
2388
2389         if ($class->isIdentifierComposite) {
2390             $id = array();
2391
2392             foreach ($class->identifier as $fieldName) {
2393                 $id[$fieldName] = isset($class->associationMappings[$fieldName])
2394                     ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']]
2395                     : $data[$fieldName];
2396             }
2397
2398             $idHash = implode(' ', $id);
2399         } else {
2400             $idHash = isset($class->associationMappings[$class->identifier[0]])
2401                 ? $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']]
2402                 : $data[$class->identifier[0]];
2403
2404             $id = array($class->identifier[0] => $idHash);
2405         }
2406
2407         if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
2408             $entity = $this->identityMap[$class->rootEntityName][$idHash];
2409             $oid = spl_object_hash($entity);
2410
2411             if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
2412                 $entity->__isInitialized__ = true;
2413                 $overrideLocalValues = true;
2414
2415                 if ($entity instanceof NotifyPropertyChanged) {
2416                     $entity->addPropertyChangedListener($this);
2417                 }
2418             } else {
2419                 $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
2420
2421                 // If only a specific entity is set to refresh, check that it's the one
2422                 if(isset($hints[Query::HINT_REFRESH_ENTITY])) {
2423                     $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity;
2424
2425                     // inject ObjectManager into just loaded proxies.
2426                     if ($overrideLocalValues && $entity instanceof ObjectManagerAware) {
2427                         $entity->injectObjectManager($this->em, $class);
2428                     }
2429                 }
2430             }
2431
2432             if ($overrideLocalValues) {
2433                 $this->originalEntityData[$oid] = $data;
2434             }
2435         } else {
2436             $entity = $this->newInstance($class);
2437             $oid    = spl_object_hash($entity);
2438
2439             $this->entityIdentifiers[$oid]  = $id;
2440             $this->entityStates[$oid]       = self::STATE_MANAGED;
2441             $this->originalEntityData[$oid] = $data;
2442
2443             $this->identityMap[$class->rootEntityName][$idHash] = $entity;
2444
2445             if ($entity instanceof NotifyPropertyChanged) {
2446                 $entity->addPropertyChangedListener($this);
2447             }
2448
2449             $overrideLocalValues = true;
2450         }
2451
2452         if ( ! $overrideLocalValues) {
2453             return $entity;
2454         }
2455
2456         foreach ($data as $field => $value) {
2457             if (isset($class->fieldMappings[$field])) {
2458                 $class->reflFields[$field]->setValue($entity, $value);
2459             }
2460         }
2461
2462         // Loading the entity right here, if its in the eager loading map get rid of it there.
2463         unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
2464
2465         if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
2466             unset($this->eagerLoadingEntities[$class->rootEntityName]);
2467         }
2468
2469         // Properly initialize any unfetched associations, if partial objects are not allowed.
2470         if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2471             return $entity;
2472         }
2473
2474         foreach ($class->associationMappings as $field => $assoc) {
2475             // Check if the association is not among the fetch-joined associations already.
2476             if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) {
2477                 continue;
2478             }
2479
2480             $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2481
2482             switch (true) {
2483                 case ($assoc['type'] & ClassMetadata::TO_ONE):
2484                     if ( ! $assoc['isOwningSide']) {
2485                         // Inverse side of x-to-one can never be lazy
2486                         $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
2487
2488                         continue 2;
2489                     }
2490
2491                     $associatedId = array();
2492
2493                     // TODO: Is this even computed right in all cases of composite keys?
2494                     foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
2495                         $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null;
2496
2497                         if ($joinColumnValue !== null) {
2498                             if ($targetClass->containsForeignIdentifier) {
2499                                 $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
2500                             } else {
2501                                 $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
2502                             }
2503                         }
2504                     }
2505
2506                     if ( ! $associatedId) {
2507                         // Foreign key is NULL
2508                         $class->reflFields[$field]->setValue($entity, null);
2509                         $this->originalEntityData[$oid][$field] = null;
2510
2511                         continue;
2512                     }
2513
2514                     if ( ! isset($hints['fetchMode'][$class->name][$field])) {
2515                         $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
2516                     }
2517
2518                     // Foreign key is set
2519                     // Check identity map first
2520                     // FIXME: Can break easily with composite keys if join column values are in
2521                     //        wrong order. The correct order is the one in ClassMetadata#identifier.
2522                     $relatedIdHash = implode(' ', $associatedId);
2523
2524                     switch (true) {
2525                         case (isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash])):
2526                             $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
2527
2528                             // If this is an uninitialized proxy, we are deferring eager loads,
2529                             // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2530                             // then we can append this entity for eager loading!
2531                             if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER &&
2532                                 isset($hints['deferEagerLoad']) &&
2533                                 !$targetClass->isIdentifierComposite &&
2534                                 $newValue instanceof Proxy &&
2535                                 $newValue->__isInitialized__ === false) {
2536
2537                                 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2538                             }
2539
2540                             break;
2541
2542                         case ($targetClass->subClasses):
2543                             // If it might be a subtype, it can not be lazy. There isn't even
2544                             // a way to solve this with deferred eager loading, which means putting
2545                             // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2546                             $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
2547                             break;
2548
2549                         default:
2550                             switch (true) {
2551                                 // We are negating the condition here. Other cases will assume it is valid!
2552                                 case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2553                                     $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2554                                     break;
2555
2556                                 // Deferred eager load only works for single identifier classes
2557                                 case (isset($hints['deferEagerLoad']) && ! $targetClass->isIdentifierComposite):
2558                                     // TODO: Is there a faster approach?
2559                                     $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
2560
2561                                     $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
2562                                     break;
2563
2564                                 default:
2565                                     // TODO: This is very imperformant, ignore it?
2566                                     $newValue = $this->em->find($assoc['targetEntity'], $associatedId);
2567                                     break;
2568                             }
2569
2570                             // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
2571                             $newValueOid = spl_object_hash($newValue);
2572                             $this->entityIdentifiers[$newValueOid] = $associatedId;
2573                             $this->identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
2574                             $this->entityStates[$newValueOid] = self::STATE_MANAGED;
2575                             // make sure that when an proxy is then finally loaded, $this->originalEntityData is set also!
2576                             break;
2577                     }
2578
2579                     $this->originalEntityData[$oid][$field] = $newValue;
2580                     $class->reflFields[$field]->setValue($entity, $newValue);
2581
2582                     if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE) {
2583                         $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
2584                         $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
2585                     }
2586
2587                     break;
2588
2589                 default:
2590                     // Inject collection
2591                     $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
2592                     $pColl->setOwner($entity, $assoc);
2593                     $pColl->setInitialized(false);
2594
2595                     $reflField = $class->reflFields[$field];
2596                     $reflField->setValue($entity, $pColl);
2597
2598                     if ($assoc['fetch'] == ClassMetadata::FETCH_EAGER) {
2599                         $this->loadCollection($pColl);
2600                         $pColl->takeSnapshot();
2601                     }
2602
2603                     $this->originalEntityData[$oid][$field] = $pColl;
2604                     break;
2605             }
2606         }
2607
2608         if ($overrideLocalValues) {
2609             if (isset($class->lifecycleCallbacks[Events::postLoad])) {
2610                 $class->invokeLifecycleCallbacks(Events::postLoad, $entity);
2611             }
2612
2613
2614             if ($this->evm->hasListeners(Events::postLoad)) {
2615                 $this->evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->em));
2616             }
2617         }
2618
2619         return $entity;
2620     }
2621
2622     /**
2623      * @return void
2624      */
2625     public function triggerEagerLoads()
2626     {
2627         if ( ! $this->eagerLoadingEntities) {
2628             return;
2629         }
2630
2631         // avoid infinite recursion
2632         $eagerLoadingEntities       = $this->eagerLoadingEntities;
2633         $this->eagerLoadingEntities = array();
2634
2635         foreach ($eagerLoadingEntities as $entityName => $ids) {
2636             if ( ! $ids) {
2637                 continue;
2638             }
2639
2640             $class = $this->em->getClassMetadata($entityName);
2641
2642             $this->getEntityPersister($entityName)->loadAll(
2643                 array_combine($class->identifier, array(array_values($ids)))
2644             );
2645         }
2646     }
2647
2648     /**
2649      * Initializes (loads) an uninitialized persistent collection of an entity.
2650      *
2651      * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2652      *
2653      * @return void
2654      * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2655      */
2656     public function loadCollection(PersistentCollection $collection)
2657     {
2658         $assoc     = $collection->getMapping();
2659         $persister = $this->getEntityPersister($assoc['targetEntity']);
2660
2661         switch ($assoc['type']) {
2662             case ClassMetadata::ONE_TO_MANY:
2663                 $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
2664                 break;
2665
2666             case ClassMetadata::MANY_TO_MANY:
2667                 $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
2668                 break;
2669         }
2670     }
2671
2672     /**
2673      * Gets the identity map of the UnitOfWork.
2674      *
2675      * @return array
2676      */
2677     public function getIdentityMap()
2678     {
2679         return $this->identityMap;
2680     }
2681
2682     /**
2683      * Gets the original data of an entity. The original data is the data that was
2684      * present at the time the entity was reconstituted from the database.
2685      *
2686      * @param object $entity
2687      *
2688      * @return array
2689      */
2690     public function getOriginalEntityData($entity)
2691     {
2692         $oid = spl_object_hash($entity);
2693
2694         if (isset($this->originalEntityData[$oid])) {
2695             return $this->originalEntityData[$oid];
2696         }
2697
2698         return array();
2699     }
2700
2701     /**
2702      * @ignore
2703      */
2704     public function setOriginalEntityData($entity, array $data)
2705     {
2706         $this->originalEntityData[spl_object_hash($entity)] = $data;
2707     }
2708
2709     /**
2710      * INTERNAL:
2711      * Sets a property value of the original data array of an entity.
2712      *
2713      * @ignore
2714      * @param string $oid
2715      * @param string $property
2716      * @param mixed $value
2717      */
2718     public function setOriginalEntityProperty($oid, $property, $value)
2719     {
2720         $this->originalEntityData[$oid][$property] = $value;
2721     }
2722
2723     /**
2724      * Gets the identifier of an entity.
2725      * The returned value is always an array of identifier values. If the entity
2726      * has a composite identifier then the identifier values are in the same
2727      * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2728      *
2729      * @param object $entity
2730      *
2731      * @return array The identifier values.
2732      */
2733     public function getEntityIdentifier($entity)
2734     {
2735         return $this->entityIdentifiers[spl_object_hash($entity)];
2736     }
2737
2738     /**
2739      * Tries to find an entity with the given identifier in the identity map of
2740      * this UnitOfWork.
2741      *
2742      * @param mixed $id The entity identifier to look for.
2743      * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
2744      *
2745      * @return mixed Returns the entity with the specified identifier if it exists in
2746      *               this UnitOfWork, FALSE otherwise.
2747      */
2748     public function tryGetById($id, $rootClassName)
2749     {
2750         $idHash = implode(' ', (array) $id);
2751
2752         if (isset($this->identityMap[$rootClassName][$idHash])) {
2753             return $this->identityMap[$rootClassName][$idHash];
2754         }
2755
2756         return false;
2757     }
2758
2759     /**
2760      * Schedules an entity for dirty-checking at commit-time.
2761      *
2762      * @param object $entity The entity to schedule for dirty-checking.
2763      * @todo Rename: scheduleForSynchronization
2764      */
2765     public function scheduleForDirtyCheck($entity)
2766     {
2767         $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
2768
2769         $this->scheduledForDirtyCheck[$rootClassName][spl_object_hash($entity)] = $entity;
2770     }
2771
2772     /**
2773      * Checks whether the UnitOfWork has any pending insertions.
2774      *
2775      * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2776      */
2777     public function hasPendingInsertions()
2778     {
2779         return ! empty($this->entityInsertions);
2780     }
2781
2782     /**
2783      * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2784      * number of entities in the identity map.
2785      *
2786      * @return integer
2787      */
2788     public function size()
2789     {
2790         $countArray = array_map(function ($item) { return count($item); }, $this->identityMap);
2791
2792         return array_sum($countArray);
2793     }
2794
2795     /**
2796      * Gets the EntityPersister for an Entity.
2797      *
2798      * @param string $entityName  The name of the Entity.
2799      *
2800      * @return \Doctrine\ORM\Persisters\BasicEntityPersister
2801      */
2802     public function getEntityPersister($entityName)
2803     {
2804         if (isset($this->persisters[$entityName])) {
2805             return $this->persisters[$entityName];
2806         }
2807
2808         $class = $this->em->getClassMetadata($entityName);
2809
2810         switch (true) {
2811             case ($class->isInheritanceTypeNone()):
2812                 $persister = new Persisters\BasicEntityPersister($this->em, $class);
2813                 break;
2814
2815             case ($class->isInheritanceTypeSingleTable()):
2816                 $persister = new Persisters\SingleTablePersister($this->em, $class);
2817                 break;
2818
2819             case ($class->isInheritanceTypeJoined()):
2820                 $persister = new Persisters\JoinedSubclassPersister($this->em, $class);
2821                 break;
2822
2823             default:
2824                 $persister = new Persisters\UnionSubclassPersister($this->em, $class);
2825         }
2826
2827         $this->persisters[$entityName] = $persister;
2828
2829         return $this->persisters[$entityName];
2830     }
2831
2832     /**
2833      * Gets a collection persister for a collection-valued association.
2834      *
2835      * @param array $association
2836      *
2837      * @return \Doctrine\ORM\Persisters\AbstractCollectionPersister
2838      */
2839     public function getCollectionPersister(array $association)
2840     {
2841         $type = $association['type'];
2842
2843         if (isset($this->collectionPersisters[$type])) {
2844             return $this->collectionPersisters[$type];
2845         }
2846
2847         switch ($type) {
2848             case ClassMetadata::ONE_TO_MANY:
2849                 $persister = new Persisters\OneToManyPersister($this->em);
2850                 break;
2851
2852             case ClassMetadata::MANY_TO_MANY:
2853                 $persister = new Persisters\ManyToManyPersister($this->em);
2854                 break;
2855         }
2856
2857         $this->collectionPersisters[$type] = $persister;
2858
2859         return $this->collectionPersisters[$type];
2860     }
2861
2862     /**
2863      * INTERNAL:
2864      * Registers an entity as managed.
2865      *
2866      * @param object $entity The entity.
2867      * @param array $id The identifier values.
2868      * @param array $data The original entity data.
2869      */
2870     public function registerManaged($entity, array $id, array $data)
2871     {
2872         $oid = spl_object_hash($entity);
2873
2874         $this->entityIdentifiers[$oid]  = $id;
2875         $this->entityStates[$oid]       = self::STATE_MANAGED;
2876         $this->originalEntityData[$oid] = $data;
2877
2878         $this->addToIdentityMap($entity);
2879
2880         if ($entity instanceof NotifyPropertyChanged) {
2881             $entity->addPropertyChangedListener($this);
2882         }
2883     }
2884
2885     /**
2886      * INTERNAL:
2887      * Clears the property changeset of the entity with the given OID.
2888      *
2889      * @param string $oid The entity's OID.
2890      */
2891     public function clearEntityChangeSet($oid)
2892     {
2893         $this->entityChangeSets[$oid] = array();
2894     }
2895
2896     /* PropertyChangedListener implementation */
2897
2898     /**
2899      * Notifies this UnitOfWork of a property change in an entity.
2900      *
2901      * @param object $entity The entity that owns the property.
2902      * @param string $propertyName The name of the property that changed.
2903      * @param mixed $oldValue The old value of the property.
2904      * @param mixed $newValue The new value of the property.
2905      */
2906     public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
2907     {
2908         $oid   = spl_object_hash($entity);
2909         $class = $this->em->getClassMetadata(get_class($entity));
2910
2911         $isAssocField = isset($class->associationMappings[$propertyName]);
2912
2913         if ( ! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
2914             return; // ignore non-persistent fields
2915         }
2916
2917         // Update changeset and mark entity for synchronization
2918         $this->entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2919
2920         if ( ! isset($this->scheduledForDirtyCheck[$class->rootEntityName][$oid])) {
2921             $this->scheduleForDirtyCheck($entity);
2922         }
2923     }
2924
2925     /**
2926      * Gets the currently scheduled entity insertions in this UnitOfWork.
2927      *
2928      * @return array
2929      */
2930     public function getScheduledEntityInsertions()
2931     {
2932         return $this->entityInsertions;
2933     }
2934
2935     /**
2936      * Gets the currently scheduled entity updates in this UnitOfWork.
2937      *
2938      * @return array
2939      */
2940     public function getScheduledEntityUpdates()
2941     {
2942         return $this->entityUpdates;
2943     }
2944
2945     /**
2946      * Gets the currently scheduled entity deletions in this UnitOfWork.
2947      *
2948      * @return array
2949      */
2950     public function getScheduledEntityDeletions()
2951     {
2952         return $this->entityDeletions;
2953     }
2954
2955     /**
2956      * Get the currently scheduled complete collection deletions
2957      *
2958      * @return array
2959      */
2960     public function getScheduledCollectionDeletions()
2961     {
2962         return $this->collectionDeletions;
2963     }
2964
2965     /**
2966      * Gets the currently scheduled collection inserts, updates and deletes.
2967      *
2968      * @return array
2969      */
2970     public function getScheduledCollectionUpdates()
2971     {
2972         return $this->collectionUpdates;
2973     }
2974
2975     /**
2976      * Helper method to initialize a lazy loading proxy or persistent collection.
2977      *
2978      * @param object
2979      *
2980      * @return void
2981      */
2982     public function initializeObject($obj)
2983     {
2984         if ($obj instanceof Proxy) {
2985             $obj->__load();
2986
2987             return;
2988         }
2989
2990         if ($obj instanceof PersistentCollection) {
2991             $obj->initialize();
2992         }
2993     }
2994
2995     /**
2996      * Helper method to show an object as string.
2997      *
2998      * @param  object $obj
2999      *
3000      * @return string
3001      */
3002     private static function objToStr($obj)
3003     {
3004         return method_exists($obj, '__toString') ? (string)$obj : get_class($obj).'@'.spl_object_hash($obj);
3005     }
3006
3007     /**
3008      * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
3009      *
3010      * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
3011      * on this object that might be necessary to perform a correct update.
3012      *
3013      *
3014      * @param object $object
3015      *
3016      * @throws ORMInvalidArgumentException
3017      *
3018      * @return void
3019      */
3020     public function markReadOnly($object)
3021     {
3022         if ( ! is_object($object) || ! $this->isInIdentityMap($object)) {
3023             throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3024         }
3025
3026         $this->readOnlyObjects[spl_object_hash($object)] = true;
3027     }
3028
3029     /**
3030      * Is this entity read only?
3031      *
3032      * @param object $object
3033      *
3034      * @throws ORMInvalidArgumentException
3035      *
3036      * @return bool
3037      */
3038     public function isReadOnly($object)
3039     {
3040         if ( ! is_object($object)) {
3041             throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
3042         }
3043
3044         return isset($this->readOnlyObjects[spl_object_hash($object)]);
3045     }
3046 }