UnitOfWork.php 79.3 KB
Newer Older
1 2
<?php
/*
romanb's avatar
romanb committed
3
 *  $Id: UnitOfWork.php 4947 2008-09-12 13:16:05Z romanb $
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
19
 * <http://www.doctrine-project.org>.
20
 */
21

22
namespace Doctrine\ORM;
23

24
use Doctrine\Common\Collections\ArrayCollection,
25
    Doctrine\Common\Collections\Collection,
romanb's avatar
romanb committed
26
    Doctrine\Common\NotifyPropertyChanged,
27
    Doctrine\Common\PropertyChangedListener,
28 29
    Doctrine\ORM\Event\LifecycleEventArgs,
    Doctrine\ORM\Proxy\Proxy;
30

31
/**
32
 * The UnitOfWork is responsible for tracking changes to objects during an
33
 * "object-level" transaction and for writing out changes to the database
34
 * in the correct order.
35 36
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
37
 * @link        www.doctrine-project.org
38
 * @since       2.0
39
 * @version     $Revision$
40 41 42
 * @author      Benjamin Eberlei <kontakt@beberlei.de>
 * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author      Jonathan Wage <jonwage@gmail.com>
43
 * @author      Roman Borschel <roman@code-factory.org>
44
 * @internal    This class contains performance-critical code.
45
 */
46
class UnitOfWork implements PropertyChangedListener
47
{
48
    /**
49
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
50 51 52 53
     */
    const STATE_MANAGED = 1;

    /**
54
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
55 56 57 58 59
     * and is not (yet) managed by an EntityManager.
     */
    const STATE_NEW = 2;

    /**
60
     * A detached entity is an instance with a persistent identity that is not
61 62 63 64 65
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
     */
    const STATE_DETACHED = 3;

    /**
66
     * A removed entity instance is an instance with a persistent identity,
67 68 69
     * associated with an EntityManager, whose persistent state has been
     * deleted (or is scheduled for deletion).
     */
70
    const STATE_REMOVED = 4;
71

72 73
    /**
     * The identity map that holds references to all managed entities that have
74
     * an identity. The entities are grouped by their class name.
75 76
     * Since all classes in a hierarchy must share the same identifier set,
     * we always take the root class name of the hierarchy.
77
     *
78
     * @var array
79
     */
80
    private $_identityMap = array();
81

82
    /**
83 84
     * Map of all identifiers of managed entities.
     * Keys are object ids (spl_object_hash).
85 86 87 88 89
     *
     * @var array
     */
    private $_entityIdentifiers = array();

90
    /**
91
     * Map of the original entity data of managed entities.
romanb's avatar
romanb committed
92 93
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
     * at commit time.
94 95
     *
     * @var array
romanb's avatar
romanb committed
96 97 98
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
     *           A value will only really be copied if the value in the entity is modified
     *           by the user.
99
     */
100
    private $_originalEntityData = array();
101 102

    /**
103
     * Map of entity changes. Keys are object ids (spl_object_hash).
104
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
105 106 107
     *
     * @var array
     */
108
    private $_entityChangeSets = array();
109 110

    /**
111
     * The (cached) states of any known entities.
romanb's avatar
romanb committed
112
     * Keys are object ids (spl_object_hash).
113 114 115
     *
     * @var array
     */
116
    private $_entityStates = array();
117

118 119
    /**
     * Map of entities that are scheduled for dirty checking at commit time.
120
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
romanb's avatar
romanb committed
121 122 123
     * Keys are object ids (spl_object_hash).
     * 
     * @var array
124
     */
125
    private $_scheduledForDirtyCheck = array();
126

127
    /**
128
     * A list of all pending entity insertions.
129 130
     *
     * @var array
131
     */
132
    private $_entityInsertions = array();
133

134
    /**
135
     * A list of all pending entity updates.
136 137
     *
     * @var array
138
     */
139
    private $_entityUpdates = array();
140 141
    
    /**
142
     * Any pending extra updates that have been scheduled by persisters.
143 144 145
     * 
     * @var array
     */
146 147
    private $_extraUpdates = array();

148
    /**
149
     * A list of all pending entity deletions.
150 151
     *
     * @var array
152
     */
153
    private $_entityDeletions = array();
154

romanb's avatar
romanb committed
155
    /**
156
     * All pending collection deletions.
romanb's avatar
romanb committed
157 158 159
     *
     * @var array
     */
160
    private $_collectionDeletions = array();
161

romanb's avatar
romanb committed
162
    /**
163
     * All pending collection updates.
romanb's avatar
romanb committed
164 165 166
     *
     * @var array
     */
167 168 169
    private $_collectionUpdates = array();

    /**
170
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
171 172 173 174 175 176
     * At the end of the UnitOfWork all these collections will make new snapshots
     * of their data.
     *
     * @var array
     */
    private $_visitedCollections = array();
177

178
    /**
179
     * The EntityManager that "owns" this UnitOfWork instance.
180
     *
181
     * @var Doctrine\ORM\EntityManager
182
     */
183
    private $_em;
184

185
    /**
186 187
     * The calculator used to calculate the order in which changes to
     * entities need to be written to the database.
188
     *
189
     * @var Doctrine\ORM\Internal\CommitOrderCalculator
190
     */
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
    private $_commitOrderCalculator;

    /**
     * The entity persister instances used to persist entity instances.
     *
     * @var array
     */
    private $_persisters = array();

    /**
     * The collection persister instances used to persist collections.
     *
     * @var array
     */
    private $_collectionPersisters = array();
206

207
    /**
208
     * EXPERIMENTAL:
209
     * Flag for whether or not to make use of the C extension.
210
     *
211
     * @var boolean
212 213
     */
    private $_useCExtension = false;
214 215
    
    /**
216
     * The EventManager used for dispatching events.
217 218 219 220
     * 
     * @var EventManager
     */
    private $_evm;
221 222
    
    /**
223
     * Orphaned entities that are scheduled for removal.
224 225 226 227
     * 
     * @var array
     */
    private $_orphanRemovals = array();
228 229
    
    //private $_readOnlyObjects = array();
230

231
    /**
232
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
233
     *
234
     * @param Doctrine\ORM\EntityManager $em
235
     */
236
    public function __construct(EntityManager $em)
237 238
    {
        $this->_em = $em;
239
        $this->_evm = $em->getEventManager();
240
        $this->_useCExtension = $this->_em->getConfiguration()->getUseCExtension();
241
    }
242

243
    /**
244
     * Commits the UnitOfWork, executing all operations that have been postponed
245 246
     * up to this point. The state of all managed entities will be synchronized with
     * the database.
romanb's avatar
romanb committed
247 248 249 250 251 252 253 254 255
     * 
     * The operations are executed in the following order:
     * 
     * 1) All entity insertions
     * 2) All entity updates
     * 3) All collection deletions
     * 4) All collection updates
     * 5) All entity deletions
     * 
256
     */
257
    public function commit()
258
    {
259
        // Compute changes done since last commit.
260 261
        $this->computeChangeSets();

262 263 264 265 266 267
        if ( ! ($this->_entityInsertions ||
                $this->_entityDeletions ||
                $this->_entityUpdates ||
                $this->_collectionUpdates ||
                $this->_collectionDeletions ||
                $this->_orphanRemovals)) {
268 269
            return; // Nothing to do.
        }
romanb's avatar
romanb committed
270

271 272 273 274 275
        if ($this->_orphanRemovals) {
            foreach ($this->_orphanRemovals as $orphan) {
                $this->remove($orphan);
            }
        }
276
        
277 278 279 280 281
        // Raise onFlush
        if ($this->_evm->hasListeners(Events::onFlush)) {
            $this->_evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->_em));
        }
        
282 283
        // Now we need a commit order to maintain referential integrity
        $commitOrder = $this->_getCommitOrder();
284

285
        $conn = $this->_em->getConnection();
286

287 288
        $conn->beginTransaction();
        try {            
289 290 291 292
            if ($this->_entityInsertions) {
                foreach ($commitOrder as $class) {
                    $this->_executeInserts($class);
                }
293
            }
294 295 296 297 298
            
            if ($this->_entityUpdates) {
                foreach ($commitOrder as $class) {
                    $this->_executeUpdates($class);
                }
299
            }
300

301 302 303 304
            // Extra updates that were requested by persisters.
            if ($this->_extraUpdates) {
                $this->_executeExtraUpdates();
            }
305 306 307 308 309 310 311 312 313 314 315 316 317

            // Collection deletions (deletions of complete collections)
            foreach ($this->_collectionDeletions as $collectionToDelete) {
                $this->getCollectionPersister($collectionToDelete->getMapping())
                        ->delete($collectionToDelete);
            }
            // Collection updates (deleteRows, updateRows, insertRows)
            foreach ($this->_collectionUpdates as $collectionToUpdate) {
                $this->getCollectionPersister($collectionToUpdate->getMapping())
                        ->update($collectionToUpdate);
            }

            // Entity deletions come last and need to be in reverse commit order
318 319 320 321
            if ($this->_entityDeletions) {
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
                    $this->_executeDeletions($commitOrder[$i]);
                }
322
            }
323

324 325
            $conn->commit();
        } catch (\Exception $e) {
326
            $conn->setRollbackOnly();
327
            $conn->rollback();
328
            $this->_em->close();
329
            throw $e;
330 331
        }

332 333
        // Take new snapshots from visited collections
        foreach ($this->_visitedCollections as $coll) {
334
            $coll->takeSnapshot();
335 336
        }

337
        // Clear up
338 339 340 341 342 343 344 345
        $this->_entityInsertions =
        $this->_entityUpdates =
        $this->_entityDeletions =
        $this->_extraUpdates =
        $this->_entityChangeSets =
        $this->_collectionUpdates =
        $this->_collectionDeletions =
        $this->_visitedCollections =
346
        $this->_scheduledForDirtyCheck =
347
        $this->_orphanRemovals = array();
348
    }
romanb's avatar
romanb committed
349 350 351 352
    
    /**
     * Executes any extra updates that have been scheduled.
     */
353
    private function _executeExtraUpdates()
354 355 356 357 358 359 360 361
    {
        foreach ($this->_extraUpdates as $oid => $update) {
            list ($entity, $changeset) = $update;
            $this->_entityChangeSets[$oid] = $changeset;
            $this->getEntityPersister(get_class($entity))->update($entity);
        }
    }

362
    /**
363
     * Gets the changeset for an entity.
364 365 366
     *
     * @return array
     */
367
    public function getEntityChangeSet($entity)
368
    {
369
        $oid = spl_object_hash($entity);
370 371
        if (isset($this->_entityChangeSets[$oid])) {
            return $this->_entityChangeSets[$oid];
372 373 374 375
        }
        return array();
    }

376
    /**
romanb's avatar
romanb committed
377
     * Computes the changes that happened to a single entity.
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
     *
     * Modifies/populates the following properties:
     *
     * {@link _originalEntityData}
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
     * then it was not fetched from the database and therefore we have no original
     * entity data yet. All of the current entity data is stored as the original entity data.
     *
     * {@link _entityChangeSets}
     * The changes detected on all properties of the entity are stored there.
     * A change is a tuple array where the first entry is the old value and the second
     * entry is the new value of the property. Changesets are used by persisters
     * to INSERT/UPDATE the persistent entity state.
     *
     * {@link _entityUpdates}
     * If the entity is already fully MANAGED (has been fetched from the database before)
     * and any changes to its properties are detected, then a reference to the entity is stored
     * there to mark it for an update.
     *
     * {@link _collectionDeletions}
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
     * then this collection is marked for deletion.
     *
     * @param ClassMetadata $class The class descriptor of the entity.
     * @param object $entity The entity for which to compute the changes.
     */
romanb's avatar
romanb committed
404
    public function computeChangeSet(Mapping\ClassMetadata $class, $entity)
405 406 407 408
    {
        if ( ! $class->isInheritanceTypeNone()) {
            $class = $this->_em->getClassMetadata(get_class($entity));
        }
romanb's avatar
romanb committed
409 410
        
        $oid = spl_object_hash($entity);
411

412
        $actualData = array();
romanb's avatar
romanb committed
413
        foreach ($class->reflFields as $name => $refProp) {
414 415 416
            if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
                $actualData[$name] = $refProp->getValue($entity);
            }
417

romanb's avatar
romanb committed
418 419
            if ($class->isCollectionValuedAssociation($name) && $actualData[$name] !== null
                    && ! ($actualData[$name] instanceof PersistentCollection)) {
romanb's avatar
romanb committed
420 421
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
                if ( ! $actualData[$name] instanceof Collection) {
422
                    $actualData[$name] = new ArrayCollection($actualData[$name]);
romanb's avatar
romanb committed
423
                }
424
                
romanb's avatar
romanb committed
425
                $assoc = $class->associationMappings[$name];
426
                
427
                // Inject PersistentCollection
428 429 430 431 432 433
                $coll = new PersistentCollection(
                    $this->_em, 
                    $this->_em->getClassMetadata($assoc->targetEntityName), 
                    $actualData[$name]
                );
                
434
                $coll->setOwner($entity, $assoc);
435
                $coll->setDirty( ! $coll->isEmpty());
436
                $class->reflFields[$name]->setValue($entity, $coll);
437 438 439
                $actualData[$name] = $coll;
            }
        }
440

441
        if ( ! isset($this->_originalEntityData[$oid])) {
442 443
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
            // These result in an INSERT.
444 445
            $this->_originalEntityData[$oid] = $actualData;
            $this->_entityChangeSets[$oid] = array_map(
446
                function($e) { return array(null, $e); }, $actualData
447 448 449 450 451 452 453 454 455 456 457 458
            );
        } else {
            // Entity is "fully" MANAGED: it was already fully persisted before
            // and we have a copy of the original data
            $originalData = $this->_originalEntityData[$oid];
            $changeSet = array();
            $entityIsDirty = false;

            foreach ($actualData as $propName => $actualValue) {
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
                if (is_object($orgValue) && $orgValue !== $actualValue) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
459
                } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
460 461 462 463
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }

                if (isset($changeSet[$propName])) {
romanb's avatar
romanb committed
464 465
                    if (isset($class->associationMappings[$propName])) {
                        $assoc = $class->associationMappings[$propName];
466 467 468 469 470 471 472
                        if ($assoc->isOneToOne()) {
                            if ($assoc->isOwningSide) {
                                $entityIsDirty = true;
                            }
                            if ($actualValue === null && $assoc->orphanRemoval) {
                                $this->scheduleOrphanRemoval($orgValue);
                            }
473 474 475 476
                        } else if ($orgValue instanceof PersistentCollection) {
                            // A PersistentCollection was de-referenced, so delete it.
                            if  ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
                                $this->_collectionDeletions[] = $orgValue;
477 478
                            }
                        }
479 480
                    } else {
                        $entityIsDirty = true;
481
                    }
482 483
                }
            }
484
            if ($changeSet) {
485 486 487
                $this->_entityChangeSets[$oid] = $changeSet;
                $this->_originalEntityData[$oid] = $actualData;

488 489 490 491
                if ($entityIsDirty) {
                    $this->_entityUpdates[$oid] = $entity;
                }
            }
492
        }
romanb's avatar
romanb committed
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
        
        // Look for changes in associations of the entity
        foreach ($class->associationMappings as $assoc) {
            $val = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
            if ($val !== null) {
                $this->_computeAssociationChanges($assoc, $val);
            }
        }
    }

    /**
     * Computes all the changes that have been done to entities and collections
     * since the last commit and stores these changes in the _entityChangeSet map
     * temporarily for access by the persisters, until the UoW commit is finished.
     */
    public function computeChangeSets()
    {
        // Compute changes for INSERTed entities first. This must always happen.
        foreach ($this->_entityInsertions as $entity) {
            $class = $this->_em->getClassMetadata(get_class($entity));
            $this->computeChangeSet($class, $entity);
        }

        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
        foreach ($this->_identityMap as $className => $entities) {
            $class = $this->_em->getClassMetadata($className);

            // Skip class if change tracking happens through notification
            if ($class->isChangeTrackingNotify() /* || $class->isReadOnly*/) {
                continue;
            }

romanb's avatar
romanb committed
525
            // If change tracking is explicit, then only compute changes on explicitly persisted entities
romanb's avatar
romanb committed
526
            $entitiesToProcess = $class->isChangeTrackingDeferredExplicit() ?
romanb's avatar
romanb committed
527 528 529
                    (isset($this->_scheduledForDirtyCheck[$className]) ?
                        $this->_scheduledForDirtyCheck[$className] : array())
                    : $entities;
romanb's avatar
romanb committed
530 531 532 533 534 535 536 537 538 539 540 541 542

            foreach ($entitiesToProcess as $entity) {
                // Ignore uninitialized proxy objects
                if (/* $entity is readOnly || */ $entity instanceof Proxy && ! $entity->__isInitialized__) {
                    continue;
                }
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
                $oid = spl_object_hash($entity);
                if ( ! isset($this->_entityInsertions[$oid]) && isset($this->_entityStates[$oid])) {
                    $this->computeChangeSet($class, $entity);
                }
            }
        }
543
    }
544

545
    /**
546
     * Computes the changes of an association.
547
     *
548 549
     * @param AssociationMapping $assoc
     * @param mixed $value The value of the association.
550
     */
551
    private function _computeAssociationChanges($assoc, $value)
552
    {
553
        if ($value instanceof PersistentCollection && $value->isDirty()) {
romanb's avatar
romanb committed
554
            if ($assoc->isOwningSide) {
555 556 557 558
                $this->_collectionUpdates[] = $value;
            }
            $this->_visitedCollections[] = $value;
        }
559

560 561
        if ( ! $assoc->isCascadePersist) {
            return; // "Persistence by reachability" only if persist cascade specified
562
        }
563
        
564 565
        // Look through the entities, and in any of their associations, for transient
        // enities, recursively. ("Persistence by reachability")
566
        if ($assoc->isOneToOne()) {
567
            if ($value instanceof Proxy && ! $value->__isInitialized__) {
568
                return; // Ignore uninitialized proxy objects
569
            }
570
            $value = array($value);
571 572
        } else if ($value instanceof PersistentCollection) {
            $value = $value->unwrap();
573
        }
574
        
romanb's avatar
romanb committed
575
        $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
576
        foreach ($value as $entry) {
577
            $state = $this->getEntityState($entry, self::STATE_NEW);
578 579
            $oid = spl_object_hash($entry);
            if ($state == self::STATE_NEW) {
romanb's avatar
romanb committed
580 581 582 583
                if (isset($targetClass->lifecycleCallbacks[Events::prePersist])) {
                    $targetClass->invokeLifecycleCallbacks(Events::prePersist, $entry);
                }
                if ($this->_evm->hasListeners(Events::prePersist)) {
584
                    $this->_evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entry, $this->_em));
romanb's avatar
romanb committed
585 586
                }
                
587
                // Get identifier, if possible (not post-insert)
588
                $idGen = $targetClass->idGenerator;
589
                if ( ! $idGen->isPostInsertGenerator()) {
590
                    $idValue = $idGen->generate($this->_em, $entry);
591
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
592
                        $this->_entityIdentifiers[$oid] = array($targetClass->identifier[0] => $idValue);
593 594 595 596 597 598
                        $targetClass->getSingleIdReflectionProperty()->setValue($entry, $idValue);
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
                    }
                    $this->addToIdentityMap($entry);
                }
romanb's avatar
romanb committed
599
                $this->_entityStates[$oid] = self::STATE_MANAGED;
600

601 602
                // NEW entities are INSERTed within the current unit of work.
                $this->_entityInsertions[$oid] = $entry;
603 604

                $this->computeChangeSet($targetClass, $entry);
605
                
606
            } else if ($state == self::STATE_REMOVED) {
607
                throw ORMException::removedEntityInCollectionDetected($entity, $assoc);
608 609 610 611 612
            }
            // MANAGED associated entities are already taken into account
            // during changeset calculation anyway, since they are in the identity map.
        }
    }
613 614
    
    /**
615
     * INTERNAL:
616 617 618
     * Computes the changeset of an individual entity, independently of the
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
     * 
romanb's avatar
romanb committed
619 620 621
     * The passed entity must be a managed entity. If the entity already has a change set
     * because this method is invoked during a commit cycle then the change sets are added.
     * whereby changes detected in this method prevail.
romanb's avatar
romanb committed
622 623
     * 
     * @ignore
romanb's avatar
romanb committed
624 625 626
     * @param ClassMetadata $class The class descriptor of the entity.
     * @param object $entity The entity for which to (re)calculate the change set.
     * @throws InvalidArgumentException If the passed entity is not MANAGED.
627
     */
628
    public function recomputeSingleEntityChangeSet($class, $entity)
629 630
    {
        $oid = spl_object_hash($entity);
romanb's avatar
romanb committed
631 632 633 634
        
        if ( ! isset($this->_entityStates[$oid]) || $this->_entityStates[$oid] != self::STATE_MANAGED) {
            throw new \InvalidArgumentException('Entity must be managed.');
        }
romanb's avatar
romanb committed
635 636 637 638 639
        
        /* TODO: Just return if changetracking policy is NOTIFY?
        if ($class->isChangeTrackingNotify()) {
            return;
        }*/
640 641 642 643 644 645 646 647 648 649 650

        if ( ! $class->isInheritanceTypeNone()) {
            $class = $this->_em->getClassMetadata(get_class($entity));
        }

        $actualData = array();
        foreach ($class->reflFields as $name => $refProp) {
            if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
                $actualData[$name] = $refProp->getValue($entity);
            }
        }
651

romanb's avatar
romanb committed
652 653 654 655 656 657 658 659 660
        $originalData = $this->_originalEntityData[$oid];
        $changeSet = array();

        foreach ($actualData as $propName => $actualValue) {
            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
            if (is_object($orgValue) && $orgValue !== $actualValue) {
                $changeSet[$propName] = array($orgValue, $actualValue);
            } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
                $changeSet[$propName] = array($orgValue, $actualValue);
661
            }
romanb's avatar
romanb committed
662 663 664 665 666
        }

        if ($changeSet) {
            if (isset($this->_entityChangeSets[$oid])) {
                $this->_entityChangeSets[$oid] = $changeSet + $this->_entityChangeSets[$oid];
667
            }
romanb's avatar
romanb committed
668
            $this->_originalEntityData[$oid] = $actualData;
669 670
        }
    }
671

romanb's avatar
romanb committed
672 673 674
    /**
     * Executes all entity insertions for entities of the specified type.
     *
675
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
676
     */
677
    private function _executeInserts($class)
678
    {
679
        $className = $class->name;
680
        $persister = $this->getEntityPersister($className);
681
        
romanb's avatar
romanb committed
682 683
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
        $hasListeners = $this->_evm->hasListeners(Events::postPersist);
684 685 686 687
        if ($hasLifecycleCallbacks || $hasListeners) {
            $entities = array();
        }
        
688
        foreach ($this->_entityInsertions as $oid => $entity) {
689
            if (get_class($entity) === $className) {
690
                $persister->addInsert($entity);
691
                unset($this->_entityInsertions[$oid]);
692 693 694
                if ($hasLifecycleCallbacks || $hasListeners) {
                    $entities[] = $entity;
                }
695 696
            }
        }
romanb's avatar
romanb committed
697

698
        $postInsertIds = $persister->executeInserts();
699

700
        if ($postInsertIds) {
romanb's avatar
romanb committed
701
            // Persister returned post-insert IDs
702 703 704 705
            foreach ($postInsertIds as $id => $entity) {
                $oid = spl_object_hash($entity);
                $idField = $class->identifier[0];
                $class->reflFields[$idField]->setValue($entity, $id);
706
                $this->_entityIdentifiers[$oid] = array($idField => $id);
707 708 709
                $this->_entityStates[$oid] = self::STATE_MANAGED;
                $this->_originalEntityData[$oid][$idField] = $id;
                $this->addToIdentityMap($entity);
710 711
            }
        }
712 713 714 715
        
        if ($hasLifecycleCallbacks || $hasListeners) {
            foreach ($entities as $entity) {
                if ($hasLifecycleCallbacks) {
romanb's avatar
romanb committed
716
                    $class->invokeLifecycleCallbacks(Events::postPersist, $entity);
717 718
                }
                if ($hasListeners) {
719
                    $this->_evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->_em));
720 721 722
                }
            }
        }
723
    }
724

romanb's avatar
romanb committed
725 726 727
    /**
     * Executes all entity updates for entities of the specified type.
     *
728
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
729
     */
730 731
    private function _executeUpdates($class)
    {
732
        $className = $class->name;
733
        $persister = $this->getEntityPersister($className);
734 735 736

        $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]);
        $hasPreUpdateListeners = $this->_evm->hasListeners(Events::preUpdate);
737 738 739
        $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]);
        $hasPostUpdateListeners = $this->_evm->hasListeners(Events::postUpdate);
        
740
        foreach ($this->_entityUpdates as $oid => $entity) {
741 742 743 744
            if (get_class($entity) == $className || $entity instanceof Proxy && $entity instanceof $className) {
                
                if ($hasPreUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(Events::preUpdate, $entity);
745
                    $this->recomputeSingleEntityChangeSet($class, $entity);
746
                }
747
                
748
                if ($hasPreUpdateListeners) {
749 750 751
                    $this->_evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
                        $entity, $this->_em, $this->_entityChangeSets[$oid])
                    );
752 753
                }

754
                $persister->update($entity);
755
                unset($this->_entityUpdates[$oid]);
756
                
757 758 759 760
                if ($hasPostUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(Events::postUpdate, $entity);
                }
                if ($hasPostUpdateListeners) {
761
                    $this->_evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->_em));
762
                }
763 764
            }
        }
765
    }
766

romanb's avatar
romanb committed
767 768 769
    /**
     * Executes all entity deletions for entities of the specified type.
     *
770
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
771
     */
772 773
    private function _executeDeletions($class)
    {
774
        $className = $class->name;
775
        $persister = $this->getEntityPersister($className);
776
                
romanb's avatar
romanb committed
777 778
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]);
        $hasListeners = $this->_evm->hasListeners(Events::postRemove);
779
        
780
        foreach ($this->_entityDeletions as $oid => $entity) {
781
            if (get_class($entity) == $className || $entity instanceof Proxy && $entity instanceof $className) {
782
                $persister->delete($entity);
783 784 785 786 787 788 789 790
                unset(
                    $this->_entityDeletions[$oid],
                    $this->_entityIdentifiers[$oid],
                    $this->_originalEntityData[$oid]
                    );
                // Entity with this $oid after deletion treated as NEW, even if the $oid
                // is obtained by a new entity because the old one went out of scope.
                $this->_entityStates[$oid] = self::STATE_NEW;
791
                
792
                if ($hasLifecycleCallbacks) {
romanb's avatar
romanb committed
793
                    $class->invokeLifecycleCallbacks(Events::postRemove, $entity);
794 795
                }
                if ($hasListeners) {
796
                    $this->_evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->_em));
797
                }
798 799 800 801 802 803 804 805 806
            }
        }
    }

    /**
     * Gets the commit order.
     *
     * @return array
     */
romanb's avatar
romanb committed
807
    private function _getCommitOrder(array $entityChangeSet = null)
808
    {
809
        if ($entityChangeSet === null) {
810
            $entityChangeSet = array_merge(
811 812
                    $this->_entityInsertions,
                    $this->_entityUpdates,
813 814
                    $this->_entityDeletions
                    );
romanb's avatar
romanb committed
815
        }
816
        
romanb's avatar
romanb committed
817 818
        $calc = $this->getCommitOrderCalculator();
        
819 820 821
        // See if there are any new classes in the changeset, that are not in the
        // commit order graph yet (dont have a node).
        $newNodes = array();
822
        foreach ($entityChangeSet as $oid => $entity) {
823
            $className = get_class($entity);         
romanb's avatar
romanb committed
824
            if ( ! $calc->hasClass($className)) {
825
                $class = $this->_em->getClassMetadata($className);
romanb's avatar
romanb committed
826
                $calc->addClass($class);
827
                $newNodes[] = $class;
828 829 830 831
            }
        }

        // Calculate dependencies for new nodes
832
        foreach ($newNodes as $class) {
833
            foreach ($class->associationMappings as $assoc) {
834
                if ($assoc->isOwningSide && $assoc->isOneToOne()) {
835
                    $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
romanb's avatar
romanb committed
836 837
                    if ( ! $calc->hasClass($targetClass->name)) {
                        $calc->addClass($targetClass);
838
                    }
romanb's avatar
romanb committed
839
                    $calc->addDependency($targetClass, $class);
840 841 842 843
                }
            }
        }

romanb's avatar
romanb committed
844
        return $calc->getCommitOrder();
845 846
    }

847
    /**
romanb's avatar
romanb committed
848
     * Schedules an entity for insertion into the database.
849
     * If the entity already has an identifier, it will be added to the identity map.
850
     *
851
     * @param object $entity The entity to schedule for insertion.
852
     */
romanb's avatar
romanb committed
853
    public function scheduleForInsert($entity)
854
    {
855
        $oid = spl_object_hash($entity);
856

857
        if (isset($this->_entityUpdates[$oid])) {
romanb's avatar
romanb committed
858
            throw new \InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
859
        }
860
        if (isset($this->_entityDeletions[$oid])) {
romanb's avatar
romanb committed
861
            throw new \InvalidArgumentException("Removed entity can not be scheduled for insertion.");
862
        }
863
        if (isset($this->_entityInsertions[$oid])) {
romanb's avatar
romanb committed
864
            throw new \InvalidArgumentException("Entity can not be scheduled for insertion twice.");
865
        }
866

867
        $this->_entityInsertions[$oid] = $entity;
romanb's avatar
romanb committed
868

869
        if (isset($this->_entityIdentifiers[$oid])) {
870 871
            $this->addToIdentityMap($entity);
        }
872
    }
873 874

    /**
875
     * Checks whether an entity is scheduled for insertion.
876
     *
877
     * @param object $entity
878 879
     * @return boolean
     */
romanb's avatar
romanb committed
880
    public function isScheduledForInsert($entity)
881
    {
882
        return isset($this->_entityInsertions[spl_object_hash($entity)]);
883
    }
884

885
    /**
886
     * Schedules an entity for being updated.
887
     *
888
     * @param object $entity The entity to schedule for being updated.
889
     */
romanb's avatar
romanb committed
890
    public function scheduleForUpdate($entity)
891
    {
892 893
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
romanb's avatar
romanb committed
894
            throw new \InvalidArgumentException("Entity has no identity.");
895
        }
896
        if (isset($this->_entityDeletions[$oid])) {
romanb's avatar
romanb committed
897
            throw new \InvalidArgumentException("Entity is removed.");
898
        }
899

900 901
        if ( ! isset($this->_entityUpdates[$oid]) && ! isset($this->_entityInsertions[$oid])) {
            $this->_entityUpdates[$oid] = $entity;
902
        }
903
    }
904 905
    
    /**
906
     * INTERNAL:
907
     * Schedules an extra update that will be executed immediately after the
908
     * regular entity updates within the currently running commit cycle.
909
     * 
romanb's avatar
romanb committed
910 911
     * Extra updates for entities are stored as (entity, changeset) tuples.
     * 
romanb's avatar
romanb committed
912
     * @ignore
romanb's avatar
romanb committed
913 914
     * @param object $entity The entity for which to schedule an extra update.
     * @param array $changeset The changeset of the entity (what to update).
915
     */
916 917
    public function scheduleExtraUpdate($entity, array $changeset)
    {
romanb's avatar
romanb committed
918 919 920 921 922 923 924
        $oid = spl_object_hash($entity);
        if (isset($this->_extraUpdates[$oid])) {
            list($ignored, $changeset2) = $this->_extraUpdates[$oid];
            $this->_extraUpdates[$oid] = array($entity, $changeset + $changeset2);
        } else {
            $this->_extraUpdates[$oid] = array($entity, $changeset);
        }
925 926
    }

927 928 929 930 931
    /**
     * Checks whether an entity is registered as dirty in the unit of work.
     * Note: Is not very useful currently as dirty entities are only registered
     * at commit time.
     *
932
     * @param object $entity
933 934
     * @return boolean
     */
romanb's avatar
romanb committed
935
    public function isScheduledForUpdate($entity)
936
    {
937
        return isset($this->_entityUpdates[spl_object_hash($entity)]);
938
    }
939 940

    /**
941 942
     * INTERNAL:
     * Schedules an entity for deletion.
romanb's avatar
romanb committed
943 944
     * 
     * @param object $entity
945
     */
romanb's avatar
romanb committed
946
    public function scheduleForDelete($entity)
947
    {
948
        $oid = spl_object_hash($entity);
949
        
950
        if (isset($this->_entityInsertions[$oid])) {
951 952 953
            if ($this->isInIdentityMap($entity)) {
                $this->removeFromIdentityMap($entity);
            }
954
            unset($this->_entityInsertions[$oid]);
955
            return; // entity has not been persisted yet, so nothing more to do.
956
        }
957

958 959 960
        if ( ! $this->isInIdentityMap($entity)) {
            return; // ignore
        }
961

962
        $this->removeFromIdentityMap($entity);
romanb's avatar
romanb committed
963

964 965
        if (isset($this->_entityUpdates[$oid])) {
            unset($this->_entityUpdates[$oid]);
romanb's avatar
romanb committed
966
        }
967 968
        if ( ! isset($this->_entityDeletions[$oid])) {
            $this->_entityDeletions[$oid] = $entity;
969
        }
970 971
    }

972
    /**
973 974
     * Checks whether an entity is registered as removed/deleted with the unit
     * of work.
975
     *
976
     * @param object $entity
977
     * @return boolean
978
     */
romanb's avatar
romanb committed
979
    public function isScheduledForDelete($entity)
980
    {
981
        return isset($this->_entityDeletions[spl_object_hash($entity)]);
982
    }
983

984
    /**
romanb's avatar
romanb committed
985
     * Checks whether an entity is scheduled for insertion, update or deletion.
986 987
     * 
     * @param $entity
romanb's avatar
romanb committed
988
     * @return boolean
989
     */
romanb's avatar
romanb committed
990
    public function isEntityScheduled($entity)
991
    {
992
        $oid = spl_object_hash($entity);
993 994 995
        return isset($this->_entityInsertions[$oid]) ||
                isset($this->_entityUpdates[$oid]) ||
                isset($this->_entityDeletions[$oid]);
996
    }
997

998
    /**
999
     * INTERNAL:
1000
     * Registers an entity in the identity map.
1001 1002 1003
     * Note that entities in a hierarchy are registered with the class name of
     * the root entity.
     *
romanb's avatar
romanb committed
1004
     * @ignore
1005
     * @param object $entity  The entity to register.
1006 1007
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
     *                  the entity in question is already managed.
1008
     */
1009
    public function addToIdentityMap($entity)
1010
    {
1011
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
1012
        $idHash = implode(' ', $this->_entityIdentifiers[spl_object_hash($entity)]);
1013
        if ($idHash === '') {
romanb's avatar
romanb committed
1014
            throw new \InvalidArgumentException("The given entity has no identity.");
1015
        }
1016
        $className = $classMetadata->rootEntityName;
1017
        if (isset($this->_identityMap[$className][$idHash])) {
1018 1019
            return false;
        }
1020
        $this->_identityMap[$className][$idHash] = $entity;
romanb's avatar
romanb committed
1021
        if ($entity instanceof NotifyPropertyChanged) {
1022 1023
            $entity->addPropertyChangedListener($this);
        }
1024 1025
        return true;
    }
1026

1027 1028
    /**
     * Gets the state of an entity within the current unit of work.
1029 1030 1031 1032
     * 
     * NOTE: This method sees entities that are not MANAGED or REMOVED and have a
     *       populated identifier, whether it is generated or manually assigned, as
     *       DETACHED. This can be incorrect for manually assigned identifiers.
1033
     *
1034
     * @param object $entity
1035 1036 1037
     * @param integer $assume The state to assume if the state is not yet known. This is usually
     *                        used to avoid costly state lookups, in the worst case with a database
     *                        lookup.
1038
     * @return int The entity state.
1039
     */
1040
    public function getEntityState($entity, $assume = null)
1041
    {
1042
        $oid = spl_object_hash($entity);
romanb's avatar
romanb committed
1043
        if ( ! isset($this->_entityStates[$oid])) {
1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054
            // State can only be NEW or DETACHED, because MANAGED/REMOVED states are immediately
            // set by the UnitOfWork directly. We treat all entities that have a populated
            // identifier as DETACHED and all others as NEW. This is not really correct for
            // manually assigned identifiers but in that case we would need to hit the database
            // and we would like to avoid that.
            if ($assume === null) {
                if ($this->_em->getClassMetadata(get_class($entity))->getIdentifierValues($entity)) {
                    $this->_entityStates[$oid] = self::STATE_DETACHED;
                } else {
                    $this->_entityStates[$oid] = self::STATE_NEW;
                }
romanb's avatar
romanb committed
1055
            } else {
1056
                $this->_entityStates[$oid] = $assume;
romanb's avatar
romanb committed
1057 1058 1059
            }
        }
        return $this->_entityStates[$oid];
1060 1061
    }

romanb's avatar
romanb committed
1062
    /**
1063
     * INTERNAL:
romanb's avatar
romanb committed
1064 1065
     * Removes an entity from the identity map. This effectively detaches the
     * entity from the persistence management of Doctrine.
romanb's avatar
romanb committed
1066
     *
romanb's avatar
romanb committed
1067
     * @ignore
1068
     * @param object $entity
1069
     * @return boolean
romanb's avatar
romanb committed
1070
     */
1071
    public function removeFromIdentityMap($entity)
1072
    {
romanb's avatar
romanb committed
1073
        $oid = spl_object_hash($entity);
1074
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
1075
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
1076
        if ($idHash === '') {
romanb's avatar
romanb committed
1077
            throw new \InvalidArgumentException("The given entity has no identity.");
1078
        }
1079
        $className = $classMetadata->rootEntityName;
1080 1081
        if (isset($this->_identityMap[$className][$idHash])) {
            unset($this->_identityMap[$className][$idHash]);
romanb's avatar
romanb committed
1082
            $this->_entityStates[$oid] = self::STATE_DETACHED;
1083 1084 1085 1086 1087
            return true;
        }

        return false;
    }
1088

romanb's avatar
romanb committed
1089
    /**
1090
     * INTERNAL:
romanb's avatar
romanb committed
1091
     * Gets an entity in the identity map by its identifier hash.
romanb's avatar
romanb committed
1092
     *
romanb's avatar
romanb committed
1093
     * @ignore
1094 1095
     * @param string $idHash
     * @param string $rootClassName
1096
     * @return object
romanb's avatar
romanb committed
1097
     */
1098
    public function getByIdHash($idHash, $rootClassName)
1099
    {
1100 1101
        return $this->_identityMap[$rootClassName][$idHash];
    }
1102 1103

    /**
1104
     * INTERNAL:
1105 1106 1107
     * Tries to get an entity by its identifier hash. If no entity is found for
     * the given hash, FALSE is returned.
     *
romanb's avatar
romanb committed
1108
     * @ignore
romanb's avatar
romanb committed
1109 1110
     * @param string $idHash
     * @param string $rootClassName
1111 1112
     * @return mixed The found entity or FALSE.
     */
1113 1114
    public function tryGetByIdHash($idHash, $rootClassName)
    {
romanb's avatar
romanb committed
1115 1116
        return isset($this->_identityMap[$rootClassName][$idHash]) ?
                $this->_identityMap[$rootClassName][$idHash] : false;
1117
    }
1118

1119
    /**
1120
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1121
     *
1122
     * @param object $entity
1123 1124
     * @return boolean
     */
1125
    public function isInIdentityMap($entity)
1126
    {
1127 1128 1129 1130
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
            return false;
        }
1131
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
1132
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
1133
        if ($idHash === '') {
1134 1135
            return false;
        }
1136
        
1137
        return isset($this->_identityMap[$classMetadata->rootEntityName][$idHash]);
1138
    }
1139

romanb's avatar
romanb committed
1140
    /**
1141
     * INTERNAL:
romanb's avatar
romanb committed
1142 1143
     * Checks whether an identifier hash exists in the identity map.
     *
romanb's avatar
romanb committed
1144
     * @ignore
romanb's avatar
romanb committed
1145 1146 1147 1148
     * @param string $idHash
     * @param string $rootClassName
     * @return boolean
     */
1149
    public function containsIdHash($idHash, $rootClassName)
1150
    {
1151
        return isset($this->_identityMap[$rootClassName][$idHash]);
1152
    }
1153 1154

    /**
1155
     * Persists an entity as part of the current unit of work.
1156
     *
1157
     * @param object $entity The entity to persist.
1158
     */
romanb's avatar
romanb committed
1159
    public function persist($entity)
1160 1161
    {
        $visited = array();
romanb's avatar
romanb committed
1162
        $this->_doPersist($entity, $visited);
1163 1164 1165 1166 1167 1168
    }

    /**
     * Saves an entity as part of the current unit of work.
     * This method is internally called during save() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
1169
     * 
1170 1171
     * NOTE: This method always considers entities that are not yet known to
     * this UnitOfWork as NEW.
1172
     *
1173
     * @param object $entity The entity to persist.
romanb's avatar
romanb committed
1174
     * @param array $visited The already visited entities.
1175
     */
romanb's avatar
romanb committed
1176
    private function _doPersist($entity, array &$visited)
1177
    {
1178
        $oid = spl_object_hash($entity);
1179
        if (isset($visited[$oid])) {
1180 1181 1182
            return; // Prevent infinite recursion
        }

1183
        $visited[$oid] = $entity; // Mark visited
1184

romanb's avatar
romanb committed
1185 1186 1187
        $class = $this->_em->getClassMetadata(get_class($entity));
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
        
1188
        switch ($entityState) {
1189
            case self::STATE_MANAGED:
1190
                // Nothing to do, except if policy is "deferred explicit"
1191
                if ($class->isChangeTrackingDeferredExplicit()) {
1192 1193
                    $this->scheduleForDirtyCheck($entity);
                }
1194
                break;
1195
            case self::STATE_NEW:
romanb's avatar
romanb committed
1196 1197
                if (isset($class->lifecycleCallbacks[Events::prePersist])) {
                    $class->invokeLifecycleCallbacks(Events::prePersist, $entity);
1198
                }
romanb's avatar
romanb committed
1199
                if ($this->_evm->hasListeners(Events::prePersist)) {
1200
                    $this->_evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->_em));
1201
                }
1202
                
romanb's avatar
romanb committed
1203
                $idGen = $class->idGenerator;
romanb's avatar
romanb committed
1204
                if ( ! $idGen->isPostInsertGenerator()) {
1205
                    $idValue = $idGen->generate($this->_em, $entity);
1206
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
1207
                        $this->_entityIdentifiers[$oid] = array($class->identifier[0] => $idValue);
1208
                        $class->setIdentifierValues($entity, $idValue);
1209 1210
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
1211
                    }
1212
                }
romanb's avatar
romanb committed
1213 1214 1215
                $this->_entityStates[$oid] = self::STATE_MANAGED;
                
                $this->scheduleForInsert($entity);
1216
                break;
1217
            case self::STATE_DETACHED:
romanb's avatar
romanb committed
1218
                throw new \InvalidArgumentException(
romanb's avatar
romanb committed
1219
                        "Behavior of persist() for a detached entity is not yet defined.");
1220
            case self::STATE_REMOVED:
1221
                // Entity becomes managed again
romanb's avatar
romanb committed
1222
                if ($this->isScheduledForDelete($entity)) {
1223
                    unset($this->_entityDeletions[$oid]);
1224 1225
                } else {
                    //FIXME: There's more to think of here...
romanb's avatar
romanb committed
1226
                    $this->scheduleForInsert($entity);
1227
                }
1228
                break;
1229
            default:
romanb's avatar
romanb committed
1230
                throw ORMException::invalidEntityState($entityState);
1231
        }
1232
        
romanb's avatar
romanb committed
1233
        $this->_cascadePersist($entity, $visited);
1234
    }
1235 1236 1237 1238

    /**
     * Deletes an entity as part of the current unit of work.
     *
1239
     * @param object $entity The entity to remove.
1240
     */
romanb's avatar
romanb committed
1241
    public function remove($entity)
1242
    {
1243
        $visited = array();
romanb's avatar
romanb committed
1244
        $this->_doRemove($entity, $visited);
1245
    }
1246

romanb's avatar
romanb committed
1247
    /**
1248
     * Deletes an entity as part of the current unit of work.
1249
     *
1250 1251
     * This method is internally called during delete() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
romanb's avatar
romanb committed
1252
     *
1253 1254
     * @param object $entity The entity to delete.
     * @param array $visited The map of the already visited entities.
1255
     * @throws InvalidArgumentException If the instance is a detached entity.
romanb's avatar
romanb committed
1256
     */
romanb's avatar
romanb committed
1257
    private function _doRemove($entity, array &$visited)
1258
    {
1259
        $oid = spl_object_hash($entity);
1260
        if (isset($visited[$oid])) {
1261 1262 1263
            return; // Prevent infinite recursion
        }

1264
        $visited[$oid] = $entity; // mark visited
1265

1266
        $class = $this->_em->getClassMetadata(get_class($entity));
1267 1268
        $entityState = $this->getEntityState($entity);
        switch ($entityState) {
1269
            case self::STATE_NEW:
1270
            case self::STATE_REMOVED:
1271
                // nothing to do
1272
                break;
1273
            case self::STATE_MANAGED:
romanb's avatar
romanb committed
1274 1275
                if (isset($class->lifecycleCallbacks[Events::preRemove])) {
                    $class->invokeLifecycleCallbacks(Events::preRemove, $entity);
1276
                }
romanb's avatar
romanb committed
1277
                if ($this->_evm->hasListeners(Events::preRemove)) {
1278
                    $this->_evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity, $this->_em));
1279
                }
romanb's avatar
romanb committed
1280
                $this->scheduleForDelete($entity);
1281
                break;
1282
            case self::STATE_DETACHED:
romanb's avatar
romanb committed
1283
                throw ORMException::detachedEntityCannotBeRemoved();
1284
            default:
romanb's avatar
romanb committed
1285
                throw ORMException::invalidEntityState($entityState);
1286
        }
1287

romanb's avatar
romanb committed
1288
        $this->_cascadeRemove($entity, $visited);
1289 1290
    }

1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308
    /**
     * Merges the state of the given detached entity into this UnitOfWork.
     *
     * @param object $entity
     * @return object The managed copy of the entity.
     */
    public function merge($entity)
    {
        $visited = array();
        return $this->_doMerge($entity, $visited);
    }

    /**
     * Executes a merge operation on an entity.
     *
     * @param object $entity
     * @param array $visited
     * @return object The managed copy of the entity.
1309 1310 1311
     * @throws OptimisticLockException If the entity uses optimistic locking through a version
     *         attribute and the version check against the managed copy fails.
     * @throws InvalidArgumentException If the entity instance is NEW.
1312 1313 1314 1315 1316 1317 1318
     */
    private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
        $id = $class->getIdentifierValues($entity);

        if ( ! $id) {
1319 1320
            throw new \InvalidArgumentException('New entity detected during merge.'
                    . ' Persist the new entity before merging.');
1321
        }
1322
        
1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
        // MANAGED entities are ignored by the merge operation
        if ($this->getEntityState($entity, self::STATE_DETACHED) == self::STATE_MANAGED) {
            $managedCopy = $entity;
        } else {
            // Try to look the entity up in the identity map.
            $managedCopy = $this->tryGetById($id, $class->rootEntityName);
            if ($managedCopy) {
                // We have the entity in-memory already, just make sure its not removed.
                if ($this->getEntityState($managedCopy) == self::STATE_REMOVED) {
                    throw new \InvalidArgumentException('Removed entity detected during merge.'
                            . ' Can not merge with a removed entity.');
                }
            } else {
                // We need to fetch the managed copy in order to merge.
                $managedCopy = $this->_em->find($class->name, $id);
1338
            }
1339 1340 1341 1342
            
            if ($managedCopy === null) {
                throw new \InvalidArgumentException('New entity detected during merge.'
                        . ' Persist the new entity before merging.');
1343
            }
1344 1345 1346 1347 1348
            
            if ($class->isVersioned) {
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
                // Throw exception if versions dont match.
1349
                if ($managedCopyVersion != $entityVersion) {
1350
                    throw OptimisticLockException::lockFailed();
1351 1352 1353 1354 1355 1356 1357 1358 1359
                }
            }
    
            // Merge state of $entity into existing (managed) entity
            foreach ($class->reflFields as $name => $prop) {
                if ( ! isset($class->associationMappings[$name])) {
                    $prop->setValue($managedCopy, $prop->getValue($entity));
                } else {
                    $assoc2 = $class->associationMappings[$name];
1360 1361 1362 1363 1364 1365 1366 1367
                    if ($assoc2->isOneToOne()) {
                        if ( ! $assoc2->isCascadeMerge) {
                            $other = $class->reflFields[$name]->getValue($entity);
                            if ($other !== null) {
                                $targetClass = $this->_em->getClassMetadata($assoc2->targetEntityName);
                                $id = $targetClass->getIdentifierValues($other);
                                $proxy = $this->_em->getProxyFactory()->getProxy($assoc2->targetEntityName, $id);
                                $prop->setValue($managedCopy, $proxy);
1368
                                $this->registerManaged($proxy, $id, array());
1369 1370
                            }
                        }
1371 1372
                    } else {
                        $coll = new PersistentCollection($this->_em,
1373 1374
                                $this->_em->getClassMetadata($assoc2->targetEntityName),
                                new ArrayCollection
1375 1376 1377 1378 1379 1380 1381
                                );
                        $coll->setOwner($managedCopy, $assoc2);
                        $coll->setInitialized($assoc2->isCascadeMerge);
                        $prop->setValue($managedCopy, $coll);
                    }
                }
                if ($class->isChangeTrackingNotify()) {
1382
                    //TODO: put changed fields in changeset...?
1383 1384 1385
                }
            }
            if ($class->isChangeTrackingDeferredExplicit()) {
1386
                //TODO: Mark $managedCopy for dirty check...? ($this->_scheduledForDirtyCheck)
1387 1388 1389 1390
            }
        }

        if ($prevManagedCopy !== null) {
romanb's avatar
romanb committed
1391
            $assocField = $assoc->sourceFieldName;
1392 1393
            $prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy));
            if ($assoc->isOneToOne()) {
1394
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1395
                //TODO: What about back-reference if bidirectional?
1396
            } else {
1397 1398
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->unwrap()->add($managedCopy);
                if ($assoc->isOneToMany()) {
1399
                    $class->reflFields[$assoc->mappedBy]->setValue($managedCopy, $prevManagedCopy);
1400
                }
1401 1402 1403 1404 1405 1406 1407
            }
        }

        $this->_cascadeMerge($entity, $managedCopy, $visited);

        return $managedCopy;
    }
romanb's avatar
romanb committed
1408
    
1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441
    /**
     * Detaches an entity from the persistence management. It's persistence will
     * no longer be managed by Doctrine.
     *
     * @param object $entity The entity to detach.
     */
    public function detach($entity)
    {
        $visited = array();
        $this->_doDetach($entity, $visited);
    }
    
    /**
     * Executes a detach operation on the given entity.
     * 
     * @param object $entity
     * @param array $visited
     * @internal This method always considers entities with an assigned identifier as DETACHED.
     */
    private function _doDetach($entity, array &$visited)
    {
        $oid = spl_object_hash($entity);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // mark visited
        
        switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
            case self::STATE_MANAGED:
                $this->removeFromIdentityMap($entity);
                unset($this->_entityInsertions[$oid], $this->_entityUpdates[$oid],
                        $this->_entityDeletions[$oid], $this->_entityIdentifiers[$oid],
romanb's avatar
romanb committed
1442
                        $this->_entityStates[$oid], $this->_originalEntityData[$oid]);
1443 1444 1445 1446 1447 1448 1449 1450 1451
                break;
            case self::STATE_NEW:
            case self::STATE_DETACHED:
                return;
        }
        
        $this->_cascadeDetach($entity, $visited);
    }
    
romanb's avatar
romanb committed
1452
    /**
1453 1454
     * Refreshes the state of the given entity from the database, overwriting
     * any local, unpersisted changes.
romanb's avatar
romanb committed
1455
     * 
1456
     * @param object $entity The entity to refresh.
1457
     * @throws InvalidArgumentException If the entity is not MANAGED.
romanb's avatar
romanb committed
1458 1459 1460 1461
     */
    public function refresh($entity)
    {
        $visited = array();
1462
        $this->_doRefresh($entity, $visited);
romanb's avatar
romanb committed
1463 1464 1465 1466 1467 1468 1469
    }
    
    /**
     * Executes a refresh operation on an entity.
     * 
     * @param object $entity The entity to refresh.
     * @param array $visited The already visited entities during cascades.
1470
     * @throws InvalidArgumentException If the entity is not MANAGED.
romanb's avatar
romanb committed
1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481
     */
    private function _doRefresh($entity, array &$visited)
    {
        $oid = spl_object_hash($entity);
        if (isset($visited[$oid])) {
            return; // Prevent infinite recursion
        }

        $visited[$oid] = $entity; // mark visited

        $class = $this->_em->getClassMetadata(get_class($entity));
1482 1483 1484 1485 1486 1487 1488
        if ($this->getEntityState($entity) == self::STATE_MANAGED) {
            $this->getEntityPersister($class->name)->refresh(
                array_combine($class->getIdentifierColumnNames(), $this->_entityIdentifiers[$oid]),
                $entity
            );
        } else {
            throw new \InvalidArgumentException("Entity is not MANAGED.");
romanb's avatar
romanb committed
1489
        }
1490
        
romanb's avatar
romanb committed
1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502
        $this->_cascadeRefresh($entity, $visited);
    }
    
    /**
     * Cascades a refresh operation to associated entities.
     *
     * @param object $entity
     * @param array $visited
     */
    private function _cascadeRefresh($entity, array &$visited)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
romanb's avatar
romanb committed
1503 1504
        foreach ($class->associationMappings as $assoc) {
            if ( ! $assoc->isCascadeRefresh) {
romanb's avatar
romanb committed
1505 1506
                continue;
            }
romanb's avatar
romanb committed
1507
            $relatedEntities = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
1508
            if ($relatedEntities instanceof Collection) {
romanb's avatar
romanb committed
1509 1510 1511 1512
                if ($relatedEntities instanceof PersistentCollection) {
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                }
romanb's avatar
romanb committed
1513 1514 1515 1516 1517 1518 1519 1520
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doRefresh($relatedEntity, $visited);
                }
            } else if ($relatedEntities !== null) {
                $this->_doRefresh($relatedEntities, $visited);
            }
        }
    }
1521 1522 1523 1524 1525 1526 1527 1528 1529 1530
    
    /**
     * Cascades a detach operation to associated entities.
     *
     * @param object $entity
     * @param array $visited
     */
    private function _cascadeDetach($entity, array &$visited)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
romanb's avatar
romanb committed
1531 1532
        foreach ($class->associationMappings as $assoc) {
            if ( ! $assoc->isCascadeDetach) {
1533 1534
                continue;
            }
romanb's avatar
romanb committed
1535
            $relatedEntities = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
1536
            if ($relatedEntities instanceof Collection) {
1537 1538 1539 1540
                if ($relatedEntities instanceof PersistentCollection) {
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                }
1541 1542 1543 1544 1545 1546 1547 1548
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doDetach($relatedEntity, $visited);
                }
            } else if ($relatedEntities !== null) {
                $this->_doDetach($relatedEntities, $visited);
            }
        }
    }
1549 1550 1551

    /**
     * Cascades a merge operation to associated entities.
1552
     *
1553 1554 1555
     * @param object $entity
     * @param object $managedCopy
     * @param array $visited
1556 1557 1558 1559
     */
    private function _cascadeMerge($entity, $managedCopy, array &$visited)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
romanb's avatar
romanb committed
1560 1561
        foreach ($class->associationMappings as $assoc) {
            if ( ! $assoc->isCascadeMerge) {
1562 1563
                continue;
            }
romanb's avatar
romanb committed
1564
            $relatedEntities = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
1565
            if ($relatedEntities instanceof Collection) {
1566 1567 1568 1569
                if ($relatedEntities instanceof PersistentCollection) {
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                }
1570
                foreach ($relatedEntities as $relatedEntity) {
romanb's avatar
romanb committed
1571
                    $this->_doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1572
                }
1573
            } else if ($relatedEntities !== null) {
romanb's avatar
romanb committed
1574
                $this->_doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1575 1576 1577 1578
            }
        }
    }

1579 1580 1581
    /**
     * Cascades the save operation to associated entities.
     *
1582
     * @param object $entity
1583
     * @param array $visited
1584
     * @param array $insertNow
1585
     */
romanb's avatar
romanb committed
1586
    private function _cascadePersist($entity, array &$visited)
1587
    {
1588
        $class = $this->_em->getClassMetadata(get_class($entity));
romanb's avatar
romanb committed
1589 1590
        foreach ($class->associationMappings as $assoc) {
            if ( ! $assoc->isCascadePersist) {
1591 1592
                continue;
            }
romanb's avatar
romanb committed
1593
            $relatedEntities = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
1594
            if (($relatedEntities instanceof Collection || is_array($relatedEntities))) {
1595 1596 1597 1598
                if ($relatedEntities instanceof PersistentCollection) {
                    // Unwrap so that foreach() does not initialize
                    $relatedEntities = $relatedEntities->unwrap();
                }
1599
                foreach ($relatedEntities as $relatedEntity) {
romanb's avatar
romanb committed
1600
                    $this->_doPersist($relatedEntity, $visited);
1601
                }
1602
            } else if ($relatedEntities !== null) {
romanb's avatar
romanb committed
1603
                $this->_doPersist($relatedEntities, $visited);
1604 1605 1606 1607
            }
        }
    }

romanb's avatar
romanb committed
1608 1609 1610
    /**
     * Cascades the delete operation to associated entities.
     *
1611
     * @param object $entity
1612
     * @param array $visited
romanb's avatar
romanb committed
1613
     */
romanb's avatar
romanb committed
1614
    private function _cascadeRemove($entity, array &$visited)
1615
    {
romanb's avatar
romanb committed
1616
        $class = $this->_em->getClassMetadata(get_class($entity));
romanb's avatar
romanb committed
1617 1618
        foreach ($class->associationMappings as $assoc) {
            if ( ! $assoc->isCascadeRemove) {
romanb's avatar
romanb committed
1619 1620
                continue;
            }
1621
            //TODO: If $entity instanceof Proxy => Initialize ?
romanb's avatar
romanb committed
1622
            $relatedEntities = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
1623
            if ($relatedEntities instanceof Collection || is_array($relatedEntities)) {
romanb's avatar
romanb committed
1624
                // If its a PersistentCollection initialization is intended! No unwrap!
romanb's avatar
romanb committed
1625
                foreach ($relatedEntities as $relatedEntity) {
romanb's avatar
romanb committed
1626
                    $this->_doRemove($relatedEntity, $visited);
romanb's avatar
romanb committed
1627
                }
1628
            } else if ($relatedEntities !== null) {
romanb's avatar
romanb committed
1629
                $this->_doRemove($relatedEntities, $visited);
romanb's avatar
romanb committed
1630 1631
            }
        }
1632
    }
romanb's avatar
romanb committed
1633

1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675
    /**
     * Acquire a lock on the given entity.
     *
     * @param object $entity
     * @param int $lockMode
     * @param int $lockVersion
     */
    public function lock($entity, $lockMode, $lockVersion = null)
    {
        $entityName = get_class($entity);
        $class = $this->_em->getClassMetadata($entityName);

        if ($lockMode == LockMode::OPTIMISTIC) {
            if (!$class->isVersioned) {
                throw OptimisticLockException::notVersioned($entityName);
            }

            if ($lockVersion != null) {
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
                if ($entityVersion != $lockVersion) {
                    throw OptimisticLockException::lockFailed();
                }
            }
        } else if ($lockMode == LockMode::PESSIMISTIC_READ || $lockMode == LockMode::PESSIMISTIC_WRITE) {

            if (!$this->_em->getConnection()->isTransactionActive()) {
                throw TransactionRequiredException::transactionRequired();
            }
            
            if ($this->getEntityState($entity) == self::STATE_MANAGED) {
                $oid = spl_object_hash($entity);

                $this->getEntityPersister($class->name)->lock(
                    array_combine($class->getIdentifierColumnNames(), $this->_entityIdentifiers[$oid]),
                    $entity
                );
            } else {
                throw new \InvalidArgumentException("Entity is not MANAGED.");
            }
        }
    }

romanb's avatar
romanb committed
1676 1677 1678 1679 1680
    /**
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
     *
     * @return Doctrine\ORM\Internal\CommitOrderCalculator
     */
1681 1682
    public function getCommitOrderCalculator()
    {
romanb's avatar
romanb committed
1683 1684 1685
        if ($this->_commitOrderCalculator === null) {
            $this->_commitOrderCalculator = new Internal\CommitOrderCalculator;
        }
1686 1687 1688
        return $this->_commitOrderCalculator;
    }

romanb's avatar
romanb committed
1689
    /**
1690
     * Clears the UnitOfWork.
romanb's avatar
romanb committed
1691
     */
1692
    public function clear()
1693
    {
romanb's avatar
romanb committed
1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704
        $this->_identityMap =
        $this->_entityIdentifiers =
        $this->_originalEntityData =
        $this->_entityChangeSets =
        $this->_entityStates =
        $this->_scheduledForDirtyCheck =
        $this->_entityInsertions =
        $this->_entityUpdates =
        $this->_entityDeletions =
        $this->_collectionDeletions =
        $this->_collectionUpdates =
romanb's avatar
romanb committed
1705
        $this->_extraUpdates =
romanb's avatar
romanb committed
1706
        $this->_orphanRemovals = array();
romanb's avatar
romanb committed
1707 1708 1709
        if ($this->_commitOrderCalculator !== null) {
            $this->_commitOrderCalculator->clear();
        }
1710
    }
1711 1712 1713 1714 1715 1716 1717
    
    /**
     * INTERNAL:
     * Schedules an orphaned entity for removal. The remove() operation will be
     * invoked on that entity at the beginning of the next commit of this
     * UnitOfWork.
     * 
romanb's avatar
romanb committed
1718
     * @ignore
1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731
     * @param object $entity
     */
    public function scheduleOrphanRemoval($entity)
    {
        $this->_orphanRemovals[spl_object_hash($entity)] = $entity;
    }
    
    /**
     * INTERNAL:
     * Schedules a complete collection for removal when this UnitOfWork commits.
     *
     * @param PersistentCollection $coll
     */
1732
    public function scheduleCollectionDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1733 1734 1735 1736 1737
    {
        //TODO: if $coll is already scheduled for recreation ... what to do?
        // Just remove $coll from the scheduled recreations?
        $this->_collectionDeletions[] = $coll;
    }
1738

1739
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1740
    {
1741
        return in_array($coll, $this->_collectionsDeletions, true);
romanb's avatar
romanb committed
1742
    }
1743

1744
    /**
1745
     * INTERNAL:
1746
     * Creates an entity. Used for reconstitution of entities during hydration.
1747
     *
romanb's avatar
romanb committed
1748
     * @ignore
1749 1750 1751 1752
     * @param string $className The name of the entity class.
     * @param array $data The data for the entity.
     * @param array $hints Any hints to account for during reconstitution/lookup of the entity.
     * @return object The entity instance.
romanb's avatar
romanb committed
1753
     * @internal Highly performance-sensitive method.
1754
     * 
1755
     * @todo Rename: getOrCreateEntity
1756
     */
1757
    public function createEntity($className, array $data, &$hints = array())
1758
    {
1759
        $class = $this->_em->getClassMetadata($className);
1760
        //$isReadOnly = isset($hints[Query::HINT_READ_ONLY]);
1761

1762 1763 1764
        if ($class->isIdentifierComposite) {
            $id = array();
            foreach ($class->identifier as $fieldName) {
1765
                $id[$fieldName] = $data[$fieldName];
1766
            }
1767
            $idHash = implode(' ', $id);
1768
        } else {
1769 1770
            $idHash = $data[$class->identifier[0]];
            $id = array($class->identifier[0] => $idHash);
1771
        }
1772 1773 1774

        if (isset($this->_identityMap[$class->rootEntityName][$idHash])) {
            $entity = $this->_identityMap[$class->rootEntityName][$idHash];
1775
            $oid = spl_object_hash($entity);
1776 1777 1778 1779 1780 1781
            if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
                $entity->__isInitialized__ = true;
                $overrideLocalValues = true;
            } else {
                $overrideLocalValues = isset($hints[Query::HINT_REFRESH]);
            }
1782
        } else {
1783
            $entity = $class->newInstance();
1784 1785
            $oid = spl_object_hash($entity);
            $this->_entityIdentifiers[$oid] = $id;
1786
            $this->_entityStates[$oid] = self::STATE_MANAGED;
1787
            $this->_originalEntityData[$oid] = $data;
1788
            $this->_identityMap[$class->rootEntityName][$idHash] = $entity;
romanb's avatar
romanb committed
1789
            if ($entity instanceof NotifyPropertyChanged) {
1790 1791
                $entity->addPropertyChangedListener($this);
            }
1792
            $overrideLocalValues = true;
1793 1794
        }

1795
        if ($overrideLocalValues) {
1796 1797 1798 1799 1800 1801 1802
            if ($this->_useCExtension) {
                doctrine_populate_data($entity, $data);
            } else {
                foreach ($data as $field => $value) {
                    if (isset($class->reflFields[$field])) {
                        $class->reflFields[$field]->setValue($entity, $value);
                    }
1803
                }
1804
            }
1805 1806 1807 1808
            
            // Properly initialize any unfetched associations, if partial objects are not allowed.
            if ( ! isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
                foreach ($class->associationMappings as $field => $assoc) {
1809
                    // Check if the association is not among the fetch-joined associations already.
romanb's avatar
romanb committed
1810
                    if (isset($hints['fetched'][$className][$field])) {
1811 1812
                        continue;
                    }
romanb's avatar
romanb committed
1813

1814
                    $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
1815

1816 1817 1818 1819 1820 1821
                    if ($assoc->isOneToOne()) {
                        if ($assoc->isOwningSide) {
                            $associatedId = array();
                            foreach ($assoc->targetToSourceKeyColumns as $targetColumn => $srcColumn) {
                                $joinColumnValue = $data[$srcColumn];
                                if ($joinColumnValue !== null) {
1822
                                    $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
1823
                                }
1824 1825
                            }
                            if ( ! $associatedId) {
1826
                                // Foreign key is NULL
1827 1828 1829
                                $class->reflFields[$field]->setValue($entity, null);
                                $this->_originalEntityData[$oid][$field] = null;
                            } else {
1830
                                // Foreign key is set
1831 1832 1833 1834 1835 1836
                                // Check identity map first
                                // FIXME: Can break easily with composite keys if join column values are in
                                //        wrong order. The correct order is the one in ClassMetadata#identifier.
                                $relatedIdHash = implode(' ', $associatedId);
                                if (isset($this->_identityMap[$targetClass->rootEntityName][$relatedIdHash])) {
                                    $newValue = $this->_identityMap[$targetClass->rootEntityName][$relatedIdHash];
1837
                                } else {
1838 1839 1840 1841 1842
                                    if ($targetClass->subClasses) {
                                        // If it might be a subtype, it can not be lazy
                                        $newValue = $assoc->load($entity, null, $this->_em, $associatedId);
                                    } else {
                                        $newValue = $this->_em->getProxyFactory()->getProxy($assoc->targetEntityName, $associatedId);
1843 1844 1845 1846 1847
                                        // PERF: Inlined & optimized code from UnitOfWork#registerManaged()
                                        $newValueOid = spl_object_hash($newValue);
                                        $this->_entityIdentifiers[$newValueOid] = $associatedId;
                                        $this->_identityMap[$targetClass->rootEntityName][$relatedIdHash] = $newValue;
                                        $this->_entityStates[$newValueOid] = self::STATE_MANAGED;
1848
                                    }
1849
                                }
1850 1851
                                $this->_originalEntityData[$oid][$field] = $newValue;
                                $class->reflFields[$field]->setValue($entity, $newValue);
1852 1853
                            }
                        } else {
1854 1855
                            // Inverse side of x-to-one can never be lazy
                            $class->reflFields[$field]->setValue($entity, $assoc->load($entity, null, $this->_em));
1856 1857 1858 1859 1860 1861
                        }
                    } else {
                        // Inject collection
                        $reflField = $class->reflFields[$field];
                        $pColl = new PersistentCollection(
                            $this->_em, $targetClass,
1862
                            //TODO: getValue might be superfluous once DDC-79 is implemented. 
1863 1864 1865 1866 1867 1868 1869 1870
                            $reflField->getValue($entity) ?: new ArrayCollection
                        );
                        $pColl->setOwner($entity, $assoc);
                        $reflField->setValue($entity, $pColl);
                        if ($assoc->isLazilyFetched()) {
                            $pColl->setInitialized(false);
                        } else {
                            $assoc->load($entity, $pColl, $this->_em);
1871
                        }
1872
                        $this->_originalEntityData[$oid][$field] = $pColl;
1873 1874 1875
                    }
                }
            }
1876
        }
1877
        
1878
        //TODO: These should be invoked later, after hydration, because associations may not yet be loaded here.
1879
        if (isset($class->lifecycleCallbacks[Events::postLoad])) {
1880 1881 1882
            $class->invokeLifecycleCallbacks(Events::postLoad, $entity);
        }
        if ($this->_evm->hasListeners(Events::postLoad)) {
1883
            $this->_evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity, $this->_em));
1884
        }
1885 1886

        return $entity;
1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897
    }

    /**
     * Gets the identity map of the UnitOfWork.
     *
     * @return array
     */
    public function getIdentityMap()
    {
        return $this->_identityMap;
    }
1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913

    /**
     * Gets the original data of an entity. The original data is the data that was
     * present at the time the entity was reconstituted from the database.
     *
     * @param object $entity
     * @return array
     */
    public function getOriginalEntityData($entity)
    {
        $oid = spl_object_hash($entity);
        if (isset($this->_originalEntityData[$oid])) {
            return $this->_originalEntityData[$oid];
        }
        return array();
    }
1914 1915 1916 1917 1918 1919 1920 1921
    
    /**
     * @ignore
     */
    public function setOriginalEntityData($entity, array $data)
    {
        $this->_originalEntityData[spl_object_hash($entity)] = $data;
    }
1922 1923 1924

    /**
     * INTERNAL:
1925
     * Sets a property value of the original data array of an entity.
1926
     *
romanb's avatar
romanb committed
1927
     * @ignore
1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938
     * @param string $oid
     * @param string $property
     * @param mixed $value
     */
    public function setOriginalEntityProperty($oid, $property, $value)
    {
        $this->_originalEntityData[$oid][$property] = $value;
    }

    /**
     * Gets the identifier of an entity.
1939
     * The returned value is always an array of identifier values. If the entity
1940
     * has a composite identifier then the identifier values are in the same
1941
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
1942 1943 1944 1945 1946 1947 1948 1949
     *
     * @param object $entity
     * @return array The identifier values.
     */
    public function getEntityIdentifier($entity)
    {
        return $this->_entityIdentifiers[spl_object_hash($entity)];
    }
1950 1951

    /**
1952 1953
     * Tries to find an entity with the given identifier in the identity map of
     * this UnitOfWork.
1954
     *
1955 1956 1957 1958
     * @param mixed $id The entity identifier to look for.
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
     * @return mixed Returns the entity with the specified identifier if it exists in
     *               this UnitOfWork, FALSE otherwise.
1959 1960 1961
     */
    public function tryGetById($id, $rootClassName)
    {
1962
        $idHash = implode(' ', (array) $id);
1963 1964 1965 1966 1967
        if (isset($this->_identityMap[$rootClassName][$idHash])) {
            return $this->_identityMap[$rootClassName][$idHash];
        }
        return false;
    }
1968

1969 1970 1971 1972 1973
    /**
     * Schedules an entity for dirty-checking at commit-time.
     *
     * @param object $entity The entity to schedule for dirty-checking.
     */
1974 1975
    public function scheduleForDirtyCheck($entity)
    {
1976
        $rootClassName = $this->_em->getClassMetadata(get_class($entity))->rootEntityName;
romanb's avatar
romanb committed
1977
        $this->_scheduledForDirtyCheck[$rootClassName][] = $entity;
1978 1979
    }

1980 1981 1982 1983 1984 1985 1986 1987 1988 1989
    /**
     * Checks whether the UnitOfWork has any pending insertions.
     *
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
     */
    public function hasPendingInsertions()
    {
        return ! empty($this->_entityInsertions);
    }

1990 1991 1992
    /**
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
     * number of entities in the identity map.
1993 1994
     *
     * @return integer
1995 1996 1997 1998
     */
    public function size()
    {
        $count = 0;
romanb's avatar
romanb committed
1999 2000 2001
        foreach ($this->_identityMap as $entitySet) {
            $count += count($entitySet);
        }
2002 2003
        return $count;
    }
2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014

    /**
     * Gets the EntityPersister for an Entity.
     *
     * @param string $entityName  The name of the Entity.
     * @return Doctrine\ORM\Persister\AbstractEntityPersister
     */
    public function getEntityPersister($entityName)
    {
        if ( ! isset($this->_persisters[$entityName])) {
            $class = $this->_em->getClassMetadata($entityName);
2015
            if ($class->isInheritanceTypeNone()) {
2016
                $persister = new Persisters\StandardEntityPersister($this->_em, $class);
2017 2018 2019
            } else if ($class->isInheritanceTypeSingleTable()) {
                $persister = new Persisters\SingleTablePersister($this->_em, $class);
            } else if ($class->isInheritanceTypeJoined()) {
2020
                $persister = new Persisters\JoinedSubclassPersister($this->_em, $class);
2021
            } else {
2022
                $persister = new Persisters\UnionSubclassPersister($this->_em, $class);
2023 2024 2025 2026 2027 2028
            }
            $this->_persisters[$entityName] = $persister;
        }
        return $this->_persisters[$entityName];
    }

2029 2030 2031 2032 2033 2034
    /**
     * Gets a collection persister for a collection-valued association.
     *
     * @param AssociationMapping $association
     * @return AbstractCollectionPersister
     */
2035 2036 2037 2038
    public function getCollectionPersister($association)
    {
        $type = get_class($association);
        if ( ! isset($this->_collectionPersisters[$type])) {
2039 2040 2041 2042
            if ($association instanceof Mapping\OneToManyMapping) {
                $persister = new Persisters\OneToManyPersister($this->_em);
            } else if ($association instanceof Mapping\ManyToManyMapping) {
                $persister = new Persisters\ManyToManyPersister($this->_em);
2043 2044 2045 2046 2047
            }
            $this->_collectionPersisters[$type] = $persister;
        }
        return $this->_collectionPersisters[$type];
    }
2048

2049 2050 2051 2052 2053 2054 2055 2056
    /**
     * INTERNAL:
     * Registers an entity as managed.
     *
     * @param object $entity The entity.
     * @param array $id The identifier values.
     * @param array $data The original entity data.
     */
romanb's avatar
romanb committed
2057
    public function registerManaged($entity, array $id, array $data)
2058 2059 2060 2061 2062 2063 2064
    {
        $oid = spl_object_hash($entity);
        $this->_entityIdentifiers[$oid] = $id;
        $this->_entityStates[$oid] = self::STATE_MANAGED;
        $this->_originalEntityData[$oid] = $data;
        $this->addToIdentityMap($entity);
    }
2065

2066 2067 2068 2069 2070 2071 2072 2073
    /**
     * INTERNAL:
     * Clears the property changeset of the entity with the given OID.
     *
     * @param string $oid The entity's OID.
     */
    public function clearEntityChangeSet($oid)
    {
romanb's avatar
romanb committed
2074
        unset($this->_entityChangeSets[$oid]);
2075
    }
2076

2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088
    /* PropertyChangedListener implementation */

    /**
     * Notifies this UnitOfWork of a property change in an entity.
     *
     * @param object $entity The entity that owns the property.
     * @param string $propertyName The name of the property that changed.
     * @param mixed $oldValue The old value of the property.
     * @param mixed $newValue The new value of the property.
     */
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
    {
2089 2090
        $oid = spl_object_hash($entity);
        $class = $this->_em->getClassMetadata(get_class($entity));
2091

2092
        $this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2093

2094 2095 2096
        if (isset($class->associationMappings[$propertyName])) {
            $assoc = $class->associationMappings[$propertyName];
            if ($assoc->isOneToOne() && $assoc->isOwningSide) {
2097
                $this->_entityUpdates[$oid] = $entity;
2098 2099 2100 2101 2102
            } else if ($oldValue instanceof PersistentCollection) {
                // A PersistentCollection was de-referenced, so delete it.
                if  ( ! in_array($oldValue, $this->_collectionDeletions, true)) {
                    $this->_collectionDeletions[] = $oldValue;
                }
2103
            }
2104 2105
        } else {
            $this->_entityUpdates[$oid] = $entity;
2106
        }
2107
    }
2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137
    
    /**
     * Gets the currently scheduled entity insertions in this UnitOfWork.
     * 
     * @return array
     */
    public function getScheduledEntityInsertions()
    {
        return $this->_entityInsertions;
    }
    
    /**
     * Gets the currently scheduled entity updates in this UnitOfWork.
     * 
     * @return array
     */
    public function getScheduledEntityUpdates()
    {
        return $this->_entityUpdates;
    }
    
    /**
     * Gets the currently scheduled entity deletions in this UnitOfWork.
     * 
     * @return array
     */
    public function getScheduledEntityDeletions()
    {
        return $this->_entityDeletions;
    }
2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157

    /**
     * Get the currently scheduled complete collection deletions
     *
     * @return array
     */
    public function getScheduledCollectionDeletions()
    {
        return $this->_collectionDeletions;
    }

    /**
     * Gets the currently scheduled collection inserts, updates and deletes.
     *
     * @return array
     */
    public function getScheduledCollectionUpdates()
    {
        return $this->_collectionUpdates;
    }
2158
}