UnitOfWork.php 70.2 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 25 26 27 28 29 30
use Doctrine\Common\Collections\ArrayCollection,
    Doctrine\Common\Collections\ICollection,
    Doctrine\Common\DoctrineException,
    Doctrine\Common\PropertyChangedListener,
    Doctrine\ORM\Event\LifecycleEventArgs,
    Doctrine\ORM\Internal\CommitOrderCalculator,
    Doctrine\ORM\Internal\CommitOrderNode;
31

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

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

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

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

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

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

89
    /**
90
     * Map of the original entity data of managed entities.
romanb's avatar
romanb committed
91 92
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
     * at commit time.
93 94
     *
     * @var array
romanb's avatar
romanb committed
95 96 97
     * @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.
98
     */
99
    private $_originalEntityData = array();
100 101

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

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

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

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

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

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

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

romanb's avatar
romanb committed
161
    /**
162
     * All pending collection creations.
romanb's avatar
romanb committed
163 164 165
     *
     * @var array
     */
romanb's avatar
romanb committed
166
    //private $_collectionCreations = array();
167

romanb's avatar
romanb committed
168
    /**
169
     * All pending collection updates.
romanb's avatar
romanb committed
170 171 172
     *
     * @var array
     */
173 174 175
    private $_collectionUpdates = array();

    /**
176
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
177 178 179 180 181 182
     * At the end of the UnitOfWork all these collections will make new snapshots
     * of their data.
     *
     * @var array
     */
    private $_visitedCollections = array();
183

184
    /**
185
     * The EntityManager that "owns" this UnitOfWork instance.
186
     *
187
     * @var Doctrine\ORM\EntityManager
188
     */
189
    private $_em;
190

191
    /**
192 193
     * The calculator used to calculate the order in which changes to
     * entities need to be written to the database.
194
     *
195
     * @var Doctrine\ORM\Internal\CommitOrderCalculator
196
     */
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
    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();
212

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

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

247
    /**
248
     * Commits the UnitOfWork, executing all operations that have been postponed
249 250
     * up to this point. The state of all managed entities will be synchronized with
     * the database.
251
     */
252
    public function commit()
253
    {
254 255
        // Compute changes done since last commit.
        // This populates _entityUpdates and _collectionUpdates.
256 257
        $this->computeChangeSets();

258 259 260 261 262 263
        if ( ! ($this->_entityInsertions ||
                $this->_entityDeletions ||
                $this->_entityUpdates ||
                $this->_collectionUpdates ||
                $this->_collectionDeletions ||
                $this->_orphanRemovals)) {
264 265 266 267 268
            return; // Nothing to do.
        }

        // Now we need a commit order to maintain referential integrity
        $commitOrder = $this->_getCommitOrder();
269 270 271 272 273 274
        
        if ($this->_orphanRemovals) {
            foreach ($this->_orphanRemovals as $orphan) {
                $this->remove($orphan);
            }
        }
275

276 277 278
        $conn = $this->_em->getConnection();
        try {
            $conn->beginTransaction();
279 280 281 282 283
            
            if ($this->_entityInsertions) {
                foreach ($commitOrder as $class) {
                    $this->_executeInserts($class);
                }
284
            }
285 286 287 288 289
            
            if ($this->_entityUpdates) {
                foreach ($commitOrder as $class) {
                    $this->_executeUpdates($class);
                }
290
            }
291

292 293 294 295
            // Extra updates that were requested by persisters.
            if ($this->_extraUpdates) {
                $this->_executeExtraUpdates();
            }
296 297 298 299 300 301 302 303 304 305 306 307 308 309

            // 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);
            }
            //TODO: collection recreations (insertions of complete collections)

            // Entity deletions come last and need to be in reverse commit order
310 311 312 313
            if ($this->_entityDeletions) {
                for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
                    $this->_executeDeletions($commitOrder[$i]);
                }
314
            }
315

316 317 318
            $conn->commit();
        } catch (\Exception $e) {
            $conn->rollback();
romanb's avatar
romanb committed
319
            $this->clear();
320
            throw $e;
321 322
        }

323 324
        // Take new snapshots from visited collections
        foreach ($this->_visitedCollections as $coll) {
325
            $coll->takeSnapshot();
326 327
        }

328
        // Clear up
329 330 331 332 333 334 335 336 337
        $this->_entityInsertions =
        $this->_entityUpdates =
        $this->_entityDeletions =
        $this->_extraUpdates =
        $this->_entityChangeSets =
        $this->_collectionUpdates =
        $this->_collectionDeletions =
        $this->_visitedCollections =
        $this->_orphanRemovals = array();
338
    }
romanb's avatar
romanb committed
339 340 341 342
    
    /**
     * Executes any extra updates that have been scheduled.
     */
343
    private function _executeExtraUpdates()
344 345 346 347 348 349 350 351
    {
        foreach ($this->_extraUpdates as $oid => $update) {
            list ($entity, $changeset) = $update;
            $this->_entityChangeSets[$oid] = $changeset;
            $this->getEntityPersister(get_class($entity))->update($entity);
        }
    }

352
    /**
353
     * Gets the changeset for an entity.
354 355 356
     *
     * @return array
     */
357
    public function getEntityChangeSet($entity)
358
    {
359
        $oid = spl_object_hash($entity);
360 361
        if (isset($this->_entityChangeSets[$oid])) {
            return $this->_entityChangeSets[$oid];
362 363 364 365 366
        }
        return array();
    }

    /**
367
     * Computes all the changes that have been done to entities and collections
368 369
     * since the last commit and stores these changes in the _entityChangeSet map
     * temporarily for access by the persisters, until the UoW commit is finished.
370 371 372
     *
     * @param array $entities The entities for which to compute the changesets. If this
     *          parameter is not specified, the changesets of all entities in the identity
373 374 375
     *          map are computed if automatic dirty checking is enabled (the default).
     *          If automatic dirty checking is disabled, only those changesets will be
     *          computed that have been scheduled through scheduleForDirtyCheck().
376
     */
romanb's avatar
romanb committed
377
    public function computeChangeSets()
378
    {
romanb's avatar
romanb committed
379
        // Compute changes for INSERTed entities first. This must always happen.
380
        foreach ($this->_entityInsertions as $entity) {
romanb's avatar
romanb committed
381 382 383 384 385 386 387 388 389
            $class = $this->_em->getClassMetadata(get_class($entity));
            $this->_computeEntityChanges($class, $entity);
            // 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);
                }
            }
390
        }
391

romanb's avatar
romanb committed
392
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
393
        foreach ($this->_identityMap as $className => $entities) {
394
            $class = $this->_em->getClassMetadata($className);
395

396
            // Skip class if change tracking happens through notification
397 398 399
            if ($class->isChangeTrackingNotify()) {
                continue;
            }
400 401

            // If change tracking is explicit, then only compute changes on explicitly saved entities
402 403 404
            $entitiesToProcess = $class->isChangeTrackingDeferredExplicit() ?
                    $this->_scheduledForDirtyCheck[$className] : $entities;

405
            foreach ($entitiesToProcess as $entity) {
romanb's avatar
romanb committed
406 407 408
                // Only MANAGED entities that are NOT INSERTED are processed here.
                $oid = spl_object_hash($entity);
                if (isset($this->_entityStates[$oid]) && ! isset($entityInsertions[$oid])) {
409 410
                    $this->_computeEntityChanges($class, $entity);
                    // Look for changes in associations of the entity
411
                    foreach ($class->associationMappings as $assoc) {
romanb's avatar
romanb committed
412
                        $val = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
413 414
                        if ($val !== null) {
                            $this->_computeAssociationChanges($assoc, $val);
415
                        }
416
                    }
417 418 419 420
                }
            }
        }
    }
421

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452
    /**
     * Computes the changes done to a single entity.
     *
     * 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.
     */
    private function _computeEntityChanges($class, $entity)
    {
        $oid = spl_object_hash($entity);
453

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

458
        $actualData = array();
romanb's avatar
romanb committed
459
        foreach ($class->reflFields as $name => $refProp) {
460 461 462
            if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
                $actualData[$name] = $refProp->getValue($entity);
            }
463

464 465
            if ($class->isCollectionValuedAssociation($name) && $actualData[$name] !== null
                    && ! ($actualData[$name] instanceof PersistentCollection)) {
romanb's avatar
romanb committed
466
                // If $actualData[$name] is Collection then unwrap the array
467 468
                if ( ! $actualData[$name] instanceof ArrayCollection) {
                    $actualData[$name] = new ArrayCollection($actualData[$name]);
romanb's avatar
romanb committed
469
                }
romanb's avatar
romanb committed
470
                $assoc = $class->associationMappings[$name];
471
                // Inject PersistentCollection
472 473
                $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata(
                        $assoc->targetEntityName), $actualData[$name]);
474
                $coll->setOwner($entity, $assoc);
475
                $coll->setDirty( ! $coll->isEmpty());
476
                $class->reflFields[$name]->setValue($entity, $coll);
477 478 479
                $actualData[$name] = $coll;
            }
        }
480

481
        if ( ! isset($this->_originalEntityData[$oid])) {
482 483
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
            // These result in an INSERT.
484 485
            $this->_originalEntityData[$oid] = $actualData;
            $this->_entityChangeSets[$oid] = array_map(
486
                function($e) { return array(null, $e); }, $actualData
487 488 489 490 491 492 493 494 495 496 497 498
            );
        } 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);
499
                } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
500 501 502 503
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }

                if (isset($changeSet[$propName])) {
romanb's avatar
romanb committed
504 505
                    if (isset($class->associationMappings[$propName])) {
                        $assoc = $class->associationMappings[$propName];
506 507 508 509 510 511 512
                        if ($assoc->isOneToOne()) {
                            if ($assoc->isOwningSide) {
                                $entityIsDirty = true;
                            }
                            if ($actualValue === null && $assoc->orphanRemoval) {
                                $this->scheduleOrphanRemoval($orgValue);
                            }
513 514 515 516
                        } else if ($orgValue instanceof PersistentCollection) {
                            // A PersistentCollection was de-referenced, so delete it.
                            if  ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
                                $this->_collectionDeletions[] = $orgValue;
517 518
                            }
                        }
519 520
                    } else {
                        $entityIsDirty = true;
521
                    }
522 523
                }
            }
524 525 526 527 528 529 530
            if ($changeSet) {
                if ($entityIsDirty) {
                    $this->_entityUpdates[$oid] = $entity;
                }
                $this->_entityChangeSets[$oid] = $changeSet;
                $this->_originalEntityData[$oid] = $actualData;
            }
531
        }
532
    }
533

534
    /**
535
     * Computes the changes of an association.
536
     *
537 538
     * @param AssociationMapping $assoc
     * @param mixed $value The value of the association.
539
     */
540
    private function _computeAssociationChanges($assoc, $value)
541
    {
542
        if ($value instanceof PersistentCollection && $value->isDirty()) {
romanb's avatar
romanb committed
543
            if ($assoc->isOwningSide) {
544 545 546 547
                $this->_collectionUpdates[] = $value;
            }
            $this->_visitedCollections[] = $value;
        }
548

549 550
        if ( ! $assoc->isCascadePersist) {
            return; // "Persistence by reachability" only if persist cascade specified
551 552 553 554
        }

        // Look through the entities, and in any of their associations, for transient
        // enities, recursively. ("Persistence by reachability")
555 556 557
        if ($assoc->isOneToOne()) {
            $value = array($value);
        }
romanb's avatar
romanb committed
558
        $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
559
        foreach ($value as $entry) {
560
            $state = $this->getEntityState($entry, self::STATE_NEW);
561 562 563
            $oid = spl_object_hash($entry);
            if ($state == self::STATE_NEW) {
                // Get identifier, if possible (not post-insert)
564
                $idGen = $targetClass->idGenerator;
565
                if ( ! $idGen->isPostInsertGenerator()) {
566
                    $idValue = $idGen->generate($this->_em, $entry);
567
                    $this->_entityStates[$oid] = self::STATE_MANAGED;
568
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) {
569 570 571 572 573 574 575 576
                        $this->_entityIdentifiers[$oid] = array($idValue);
                        $targetClass->getSingleIdReflectionProperty()->setValue($entry, $idValue);
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
                    }
                    $this->addToIdentityMap($entry);
                }

577
                // Collect the original data and changeset, recursing into associations.
578
                $data = array();
579
                $changeSet = array();
romanb's avatar
romanb committed
580
                foreach ($targetClass->reflFields as $name => $refProp) {
581
                    $data[$name] = $refProp->getValue($entry);
582
                    $changeSet[$name] = array(null, $data[$name]);
romanb's avatar
romanb committed
583
                    if (isset($targetClass->associationMappings[$name])) {
584
                        //TODO: Prevent infinite recursion
romanb's avatar
romanb committed
585
                        $this->_computeAssociationChanges($targetClass->associationMappings[$name], $data[$name]);
586
                    }
587
                }
588

589 590
                // NEW entities are INSERTed within the current unit of work.
                $this->_entityInsertions[$oid] = $entry;
591
                $this->_entityChangeSets[$oid] = $changeSet;
592
                $this->_originalEntityData[$oid] = $data;
593 594
            } else if ($state == self::STATE_REMOVED) {
                throw DoctrineException::updateMe("Removed entity in collection detected during flush."
595
                        . " Make sure you properly remove deleted entities from collections.");
596 597 598 599 600
            }
            // MANAGED associated entities are already taken into account
            // during changeset calculation anyway, since they are in the identity map.
        }
    }
601 602
    
    /**
603
     * INTERNAL, EXPERIMENTAL:
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648
     * Computes the changeset of an individual entity, independently of the
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
     * 
     * @param $class
     * @param $entity
     */
    public function computeSingleEntityChangeSet($class, $entity)
    {
        $oid = spl_object_hash($entity);

        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);
            }
        }
        
        if ( ! isset($this->_originalEntityData[$oid])) {
            $this->_originalEntityData[$oid] = $actualData;
            $this->_entityChangeSets[$oid] = array_map(
                function($e) { return array(null, $e); }, $actualData
            );
        } else {
            $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);
                }
            }
            
            if ($changeSet) {
                $this->_entityChangeSets[$oid] = $changeSet;
                $this->_originalEntityData[$oid] = $actualData;
            }
        }
    }
649

romanb's avatar
romanb committed
650 651 652
    /**
     * Executes all entity insertions for entities of the specified type.
     *
653
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
654
     */
655
    private function _executeInserts($class)
656
    {
657
        $className = $class->name;
658
        $persister = $this->getEntityPersister($className);
659
        
romanb's avatar
romanb committed
660 661
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
        $hasListeners = $this->_evm->hasListeners(Events::postPersist);
662 663 664 665
        if ($hasLifecycleCallbacks || $hasListeners) {
            $entities = array();
        }
        
666
        foreach ($this->_entityInsertions as $oid => $entity) {
667
            if (get_class($entity) == $className) {
668
                $persister->addInsert($entity);
669
                unset($this->_entityInsertions[$oid]);
670 671 672
                if ($hasLifecycleCallbacks || $hasListeners) {
                    $entities[] = $entity;
                }
673 674
            }
        }
675
        
676
        $postInsertIds = $persister->executeInserts();
677
        
678
        if ($postInsertIds) {
679
            // Persister returned a post-insert IDs
680 681 682 683 684 685 686 687
            foreach ($postInsertIds as $id => $entity) {
                $oid = spl_object_hash($entity);
                $idField = $class->identifier[0];
                $class->reflFields[$idField]->setValue($entity, $id);
                $this->_entityIdentifiers[$oid] = array($id);
                $this->_entityStates[$oid] = self::STATE_MANAGED;
                $this->_originalEntityData[$oid][$idField] = $id;
                $this->addToIdentityMap($entity);
688 689
            }
        }
690 691 692 693
        
        if ($hasLifecycleCallbacks || $hasListeners) {
            foreach ($entities as $entity) {
                if ($hasLifecycleCallbacks) {
romanb's avatar
romanb committed
694
                    $class->invokeLifecycleCallbacks(Events::postPersist, $entity);
695 696
                }
                if ($hasListeners) {
romanb's avatar
romanb committed
697
                    $this->_evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity));
698 699 700
                }
            }
        }
701
    }
702

romanb's avatar
romanb committed
703 704 705
    /**
     * Executes all entity updates for entities of the specified type.
     *
706
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
707
     */
708 709
    private function _executeUpdates($class)
    {
710
        $className = $class->name;
711
        $persister = $this->getEntityPersister($className);
712 713 714 715 716 717
        
        $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]);
        $hasPreUpdateListeners = $this->_evm->hasListeners(Events::preUpdate);
        $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]);
        $hasPostUpdateListeners = $this->_evm->hasListeners(Events::postUpdate);
        
718
        foreach ($this->_entityUpdates as $oid => $entity) {
719
            if (get_class($entity) == $className) {
720 721 722 723 724 725 726 727 728 729 730 731
                if ($hasPreUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(Events::preUpdate, $entity);
                    if ( ! $hasPreUpdateListeners) {
                        // Need to recompute entity changeset to detect changes made in the callback.
                        $this->computeSingleEntityChangeSet($class, $entity);
                    }
                }
                if ($hasPreUpdateListeners) {
                    $this->_evm->dispatchEvent(Events::preUpdate, new LifecycleEventArgs($entity));
                    // Need to recompute entity changeset to detect changes made in the listener.
                    $this->computeSingleEntityChangeSet($class, $entity);
                }
732
                
733
                $persister->update($entity);
734
                unset($this->_entityUpdates[$oid]);
735
                
736 737 738 739 740 741
                if ($hasPostUpdateLifecycleCallbacks) {
                    $class->invokeLifecycleCallbacks(Events::postUpdate, $entity);
                }
                if ($hasPostUpdateListeners) {
                    $this->_evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity));
                }
742 743
            }
        }
744
    }
745

romanb's avatar
romanb committed
746 747 748
    /**
     * Executes all entity deletions for entities of the specified type.
     *
749
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
750
     */
751 752
    private function _executeDeletions($class)
    {
753
        $className = $class->name;
754
        $persister = $this->getEntityPersister($className);
755
                
romanb's avatar
romanb committed
756 757
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]);
        $hasListeners = $this->_evm->hasListeners(Events::postRemove);
758
        
759
        foreach ($this->_entityDeletions as $oid => $entity) {
760
            if (get_class($entity) == $className) {
761
                $persister->delete($entity);
762
                unset($this->_entityDeletions[$oid]);
763
                
764
                if ($hasLifecycleCallbacks) {
romanb's avatar
romanb committed
765
                    $class->invokeLifecycleCallbacks(Events::postRemove, $entity);
766 767
                }
                if ($hasListeners) {
romanb's avatar
romanb committed
768
                    $this->_evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity));
769
                }
770 771 772 773 774 775 776 777 778
            }
        }
    }

    /**
     * Gets the commit order.
     *
     * @return array
     */
romanb's avatar
romanb committed
779
    private function _getCommitOrder(array $entityChangeSet = null)
780
    {
781
        if ($entityChangeSet === null) {
782
            $entityChangeSet = array_merge(
783 784
                    $this->_entityInsertions,
                    $this->_entityUpdates,
785 786
                    $this->_entityDeletions
                    );
romanb's avatar
romanb committed
787
        }
788 789 790

        // TODO: We can cache computed commit orders in the metadata cache!
        // Check cache at this point here!
791

792 793 794 795
        // 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();
        foreach ($entityChangeSet as $entity) {
796
            $className = get_class($entity);
797 798 799 800
            if ( ! $this->_commitOrderCalculator->hasClass($className)) {
                $class = $this->_em->getClassMetadata($className);
                $this->_commitOrderCalculator->addClass($class);
                $newNodes[] = $class;
801 802 803 804
            }
        }

        // Calculate dependencies for new nodes
805
        foreach ($newNodes as $class) {
806
            foreach ($class->associationMappings as $assocMapping) {
807
                //TODO: should skip target classes that are not in the changeset.
romanb's avatar
romanb committed
808 809
                if ($assocMapping->isOwningSide) {
                    $targetClass = $this->_em->getClassMetadata($assocMapping->targetEntityName);
810 811
                    if ( ! $this->_commitOrderCalculator->hasClass($targetClass->name)) {
                        $this->_commitOrderCalculator->addClass($targetClass);
812 813
                    }
                    // add dependency
814
                    $this->_commitOrderCalculator->addDependency($targetClass, $class);
815 816 817 818 819 820 821
                }
            }
        }

        return $this->_commitOrderCalculator->getCommitOrder();
    }

822
    /**
romanb's avatar
romanb committed
823
     * Schedules an entity for insertion into the database.
824
     * If the entity already has an identifier, it will be added to the identity map.
825
     *
826
     * @param object $entity
827
     */
romanb's avatar
romanb committed
828
    public function scheduleForInsert($entity)
829
    {
830
        $oid = spl_object_hash($entity);
831

832
        if (isset($this->_entityUpdates[$oid])) {
833
            throw DoctrineException::updateMe("Dirty object can't be registered as new.");
834
        }
835
        if (isset($this->_entityDeletions[$oid])) {
836
            throw DoctrineException::updateMe("Removed object can't be registered as new.");
837
        }
838
        if (isset($this->_entityInsertions[$oid])) {
839
            throw DoctrineException::updateMe("Object already registered as new. Can't register twice.");
840
        }
841

842
        $this->_entityInsertions[$oid] = $entity;
843
        if (isset($this->_entityIdentifiers[$oid])) {
844 845
            $this->addToIdentityMap($entity);
        }
846
    }
847 848

    /**
849
     * Checks whether an entity is registered as new on this unit of work.
850
     *
851
     * @param object $entity
852 853
     * @return boolean
     */
romanb's avatar
romanb committed
854
    public function isScheduledForInsert($entity)
855
    {
856
        return isset($this->_entityInsertions[spl_object_hash($entity)]);
857
    }
858

859 860
    /**
     * Registers a dirty entity.
861
     *
862
     * @param object $entity
863
     */
romanb's avatar
romanb committed
864
    public function scheduleForUpdate($entity)
865
    {
866 867
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
868
            throw DoctrineException::updateMe("Entity without identity "
869 870
                    . "can't be registered as dirty.");
        }
871
        if (isset($this->_entityDeletions[$oid])) {
872
            throw DoctrineException::updateMe("Removed object can't be registered as dirty.");
873
        }
874

875 876
        if ( ! isset($this->_entityUpdates[$oid]) && ! isset($this->_entityInsertions[$oid])) {
            $this->_entityUpdates[$oid] = $entity;
877
        }
878
    }
879 880
    
    /**
881
     * INTERNAL:
882 883 884 885 886 887
     * Schedules an extra update that will be executed immediately after the
     * regular entity updates.
     * 
     * @param $entity
     * @param $changeset
     */
888 889 890 891 892
    public function scheduleExtraUpdate($entity, array $changeset)
    {
        $this->_extraUpdates[spl_object_hash($entity)] = array($entity, $changeset);
    }

893 894 895 896 897
    /**
     * 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.
     *
898
     * @param object $entity
899 900
     * @return boolean
     */
romanb's avatar
romanb committed
901
    public function isScheduledForUpdate($entity)
902
    {
903
        return isset($this->_entityUpdates[spl_object_hash($entity)]);
904
    }
905 906

    /**
907
     * Registers a deleted entity.
romanb's avatar
romanb committed
908 909
     * 
     * @param object $entity
910
     */
romanb's avatar
romanb committed
911
    public function scheduleForDelete($entity)
912
    {
913
        $oid = spl_object_hash($entity);
914
        if ( ! $this->isInIdentityMap($entity)) {
915 916
            return;
        }
917 918

        $this->removeFromIdentityMap($entity);
919
        $className = get_class($entity);
920

921 922
        if (isset($this->_entityInsertions[$oid])) {
            unset($this->_entityInsertions[$oid]);
923
            return; // entity has not been persisted yet, so nothing more to do.
924
        }
romanb's avatar
romanb committed
925

926 927
        if (isset($this->_entityUpdates[$oid])) {
            unset($this->_entityUpdates[$oid]);
romanb's avatar
romanb committed
928
        }
929 930
        if ( ! isset($this->_entityDeletions[$oid])) {
            $this->_entityDeletions[$oid] = $entity;
931
        }
932 933
    }

934
    /**
935 936
     * Checks whether an entity is registered as removed/deleted with the unit
     * of work.
937
     *
938
     * @param object $entity
939
     * @return boolean
940
     */
romanb's avatar
romanb committed
941
    public function isScheduledForDelete($entity)
942
    {
943
        return isset($this->_entityDeletions[spl_object_hash($entity)]);
944
    }
945

946
    /**
romanb's avatar
romanb committed
947
     * Checks whether an entity is scheduled for insertion, update or deletion.
948 949
     * 
     * @param $entity
romanb's avatar
romanb committed
950
     * @return boolean
951
     */
romanb's avatar
romanb committed
952
    public function isEntityScheduled($entity)
953
    {
954
        $oid = spl_object_hash($entity);
955 956 957
        return isset($this->_entityInsertions[$oid]) ||
                isset($this->_entityUpdates[$oid]) ||
                isset($this->_entityDeletions[$oid]);
958
    }
959

960
    /**
961
     * INTERNAL:
962
     * Registers an entity in the identity map.
963 964 965
     * Note that entities in a hierarchy are registered with the class name of
     * the root entity.
     *
966
     * @param object $entity  The entity to register.
967 968
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
     *                  the entity in question is already managed.
969
     */
970
    public function addToIdentityMap($entity)
971
    {
972
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
973
        $idHash = implode(' ', $this->_entityIdentifiers[spl_object_hash($entity)]);
974
        if ($idHash === '') {
975
            throw DoctrineException::updateMe("Entity with oid '" . spl_object_hash($entity)
976
                    . "' has no identity and therefore can't be added to the identity map.");
977
        }
978
        $className = $classMetadata->rootEntityName;
979
        if (isset($this->_identityMap[$className][$idHash])) {
980 981
            return false;
        }
982
        $this->_identityMap[$className][$idHash] = $entity;
983 984 985
        if ($entity instanceof \Doctrine\Common\NotifyPropertyChanged) {
            $entity->addPropertyChangedListener($this);
        }
986 987
        return true;
    }
988

989 990
    /**
     * Gets the state of an entity within the current unit of work.
991 992 993 994
     * 
     * 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.
995
     *
996
     * @param object $entity
997 998 999
     * @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.
1000
     * @return int The entity state.
1001
     */
1002
    public function getEntityState($entity, $assume = null)
1003
    {
1004
        $oid = spl_object_hash($entity);
romanb's avatar
romanb committed
1005
        if ( ! isset($this->_entityStates[$oid])) {
1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
            // 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
1017
            } else {
1018
                $this->_entityStates[$oid] = $assume;
romanb's avatar
romanb committed
1019 1020 1021
            }
        }
        return $this->_entityStates[$oid];
1022 1023
    }

romanb's avatar
romanb committed
1024
    /**
1025
     * INTERNAL:
romanb's avatar
romanb committed
1026 1027
     * Removes an entity from the identity map. This effectively detaches the
     * entity from the persistence management of Doctrine.
romanb's avatar
romanb committed
1028
     *
1029
     * @param object $entity
1030
     * @return boolean
romanb's avatar
romanb committed
1031
     */
1032
    public function removeFromIdentityMap($entity)
1033
    {
romanb's avatar
romanb committed
1034
        $oid = spl_object_hash($entity);
1035
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
1036
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
1037
        if ($idHash === '') {
1038
            throw DoctrineException::updateMe("Entity with oid '" . spl_object_hash($entity)
1039
                    . "' has no identity and therefore can't be removed from the identity map.");
1040
        }
1041
        $className = $classMetadata->rootEntityName;
1042 1043
        if (isset($this->_identityMap[$className][$idHash])) {
            unset($this->_identityMap[$className][$idHash]);
romanb's avatar
romanb committed
1044
            $this->_entityStates[$oid] = self::STATE_DETACHED;
1045 1046 1047 1048 1049
            return true;
        }

        return false;
    }
1050

romanb's avatar
romanb committed
1051
    /**
1052
     * INTERNAL:
romanb's avatar
romanb committed
1053
     * Gets an entity in the identity map by its identifier hash.
romanb's avatar
romanb committed
1054
     *
1055 1056
     * @param string $idHash
     * @param string $rootClassName
1057
     * @return object
romanb's avatar
romanb committed
1058
     */
1059
    public function getByIdHash($idHash, $rootClassName)
1060
    {
1061 1062
        return $this->_identityMap[$rootClassName][$idHash];
    }
1063 1064

    /**
1065
     * INTERNAL:
1066 1067 1068
     * 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
1069 1070
     * @param string $idHash
     * @param string $rootClassName
1071 1072
     * @return mixed The found entity or FALSE.
     */
1073 1074 1075 1076 1077 1078 1079
    public function tryGetByIdHash($idHash, $rootClassName)
    {
        if ($this->containsIdHash($idHash, $rootClassName)) {
            return $this->getByIdHash($idHash, $rootClassName);
        }
        return false;
    }
1080

1081
    /**
1082
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1083
     *
1084
     * @param object $entity
1085 1086
     * @return boolean
     */
1087
    public function isInIdentityMap($entity)
1088
    {
1089 1090 1091 1092
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
            return false;
        }
1093
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
1094
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
1095
        if ($idHash === '') {
1096 1097
            return false;
        }
1098 1099

        return isset($this->_identityMap[$classMetadata->rootEntityName][$idHash]);
1100
    }
1101

romanb's avatar
romanb committed
1102
    /**
1103
     * INTERNAL:
romanb's avatar
romanb committed
1104 1105 1106 1107 1108 1109
     * Checks whether an identifier hash exists in the identity map.
     *
     * @param string $idHash
     * @param string $rootClassName
     * @return boolean
     */
1110
    public function containsIdHash($idHash, $rootClassName)
1111
    {
1112
        return isset($this->_identityMap[$rootClassName][$idHash]);
1113
    }
1114 1115

    /**
1116
     * Persists an entity as part of the current unit of work.
1117
     *
1118
     * @param object $entity The entity to persist.
1119
     */
romanb's avatar
romanb committed
1120
    public function persist($entity)
1121 1122
    {
        $visited = array();
romanb's avatar
romanb committed
1123
        $this->_doPersist($entity, $visited);
1124 1125 1126 1127 1128 1129
    }

    /**
     * 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.
1130 1131
     * 
     * NOTE: This method always considers entities with a manually assigned identifier as NEW.
1132
     *
1133
     * @param object $entity The entity to persist.
romanb's avatar
romanb committed
1134
     * @param array $visited The already visited entities.
1135
     */
romanb's avatar
romanb committed
1136
    private function _doPersist($entity, array &$visited)
1137
    {
1138
        $oid = spl_object_hash($entity);
1139
        if (isset($visited[$oid])) {
1140 1141 1142
            return; // Prevent infinite recursion
        }

1143
        $visited[$oid] = $entity; // Mark visited
1144

1145 1146
        $class = $this->_em->getClassMetadata(get_class($entity));        
        switch ($this->getEntityState($entity, self::STATE_NEW)) {
1147
            case self::STATE_MANAGED:
1148
                // Nothing to do, except if policy is "deferred explicit"
1149
                if ($class->isChangeTrackingDeferredExplicit()) {
1150 1151
                    $this->scheduleForDirtyCheck($entity);
                }
1152
                break;
1153
            case self::STATE_NEW:
romanb's avatar
romanb committed
1154 1155
                if (isset($class->lifecycleCallbacks[Events::prePersist])) {
                    $class->invokeLifecycleCallbacks(Events::prePersist, $entity);
1156
                }
romanb's avatar
romanb committed
1157 1158
                if ($this->_evm->hasListeners(Events::prePersist)) {
                    $this->_evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity));
1159
                }
1160
                
romanb's avatar
romanb committed
1161
                $idGen = $class->idGenerator;
romanb's avatar
romanb committed
1162
                if ( ! $idGen->isPostInsertGenerator()) {
1163
                    $idValue = $idGen->generate($this->_em, $entity);
1164
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) {
1165
                        $this->_entityIdentifiers[$oid] = array($idValue);
1166
                        $class->setIdentifierValues($entity, $idValue);
1167 1168
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
1169
                    }
1170
                }
romanb's avatar
romanb committed
1171 1172 1173
                $this->_entityStates[$oid] = self::STATE_MANAGED;
                
                $this->scheduleForInsert($entity);
1174
                break;
1175
            case self::STATE_DETACHED:
1176
                throw DoctrineException::updateMe("Behavior of save() for a detached entity "
1177
                        . "is not yet defined.");
1178
            case self::STATE_REMOVED:
1179
                // Entity becomes managed again
romanb's avatar
romanb committed
1180
                if ($this->isScheduledForDelete($entity)) {
1181
                    unset($this->_entityDeletions[$oid]);
1182 1183
                } else {
                    //FIXME: There's more to think of here...
romanb's avatar
romanb committed
1184
                    $this->scheduleForInsert($entity);
1185
                }
1186
                break;
1187
            default:
1188
                throw DoctrineException::updateMe("Encountered invalid entity state.");
1189
        }
1190
        
romanb's avatar
romanb committed
1191
        $this->_cascadePersist($entity, $visited);
1192
    }
1193 1194 1195 1196

    /**
     * Deletes an entity as part of the current unit of work.
     *
1197
     * @param object $entity The entity to remove.
1198
     */
romanb's avatar
romanb committed
1199
    public function remove($entity)
1200
    {
1201
        $visited = array();
romanb's avatar
romanb committed
1202
        $this->_doRemove($entity, $visited);
1203
    }
1204

romanb's avatar
romanb committed
1205
    /**
1206
     * Deletes an entity as part of the current unit of work.
1207
     *
1208 1209
     * This method is internally called during delete() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
romanb's avatar
romanb committed
1210
     *
1211 1212
     * @param object $entity The entity to delete.
     * @param array $visited The map of the already visited entities.
1213
     * @throws InvalidArgumentException If the instance is a detached entity.
romanb's avatar
romanb committed
1214
     */
romanb's avatar
romanb committed
1215
    private function _doRemove($entity, array &$visited)
1216
    {
1217
        $oid = spl_object_hash($entity);
1218
        if (isset($visited[$oid])) {
1219 1220 1221
            return; // Prevent infinite recursion
        }

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

1224
        $class = $this->_em->getClassMetadata(get_class($entity));
1225 1226
        switch ($this->getEntityState($entity)) {
            case self::STATE_NEW:
1227
            case self::STATE_REMOVED:
1228
                // nothing to do
1229
                break;
1230
            case self::STATE_MANAGED:
romanb's avatar
romanb committed
1231 1232
                if (isset($class->lifecycleCallbacks[Events::preRemove])) {
                    $class->invokeLifecycleCallbacks(Events::preRemove, $entity);
1233
                }
romanb's avatar
romanb committed
1234 1235
                if ($this->_evm->hasListeners(Events::preRemove)) {
                    $this->_evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($entity));
1236
                }
romanb's avatar
romanb committed
1237
                $this->scheduleForDelete($entity);
1238
                break;
1239
            case self::STATE_DETACHED:
1240
                throw new \InvalidArgumentException("A detached entity can not be removed.");
1241
            default:
1242
                throw DoctrineException::updateMe("Encountered invalid entity state.");
1243
        }
1244
        
romanb's avatar
romanb committed
1245
        $this->_cascadeRemove($entity, $visited);
1246 1247
    }

1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
    /**
     * 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.
1266 1267 1268
     * @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.
1269 1270 1271 1272 1273 1274 1275
     */
    private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
        $id = $class->getIdentifierValues($entity);

        if ( ! $id) {
1276 1277
            throw new \InvalidArgumentException('New entity detected during merge.'
                    . ' Persist the new entity before merging.');
1278
        }
1279
        
1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294
        // 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);
1295
            }
1296 1297 1298 1299
            
            if ($managedCopy === null) {
                throw new \InvalidArgumentException('New entity detected during merge.'
                        . ' Persist the new entity before merging.');
1300
            }
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324
            
            if ($class->isVersioned) {
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
                $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
                // Throw exception if versions dont match.
                if ($managedCopyVersion != $entity) {
                    throw OptimisticLockException::versionMismatch();
                }
            }
    
            // 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];
                    if ($assoc2->isOneToOne() && ! $assoc2->isCascadeMerge) {
                        //TODO: Only do this when allowPartialObjects == false?
                        $targetClass = $this->_em->getClassMetadata($assoc2->targetEntityName);
                        $prop->setValue($managedCopy, $this->_em->getProxyFactory()
                                ->getReferenceProxy($assoc2->targetEntityName, $targetClass->getIdentifierValues($entity)));
                    } else {
                        //TODO: Only do this when allowPartialObjects == false?
                        $coll = new PersistentCollection($this->_em,
1325 1326
                                $this->_em->getClassMetadata($assoc2->targetEntityName),
                                new ArrayCollection
1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337
                                );
                        $coll->setOwner($managedCopy, $assoc2);
                        $coll->setInitialized($assoc2->isCascadeMerge);
                        $prop->setValue($managedCopy, $coll);
                    }
                }
                if ($class->isChangeTrackingNotify()) {
                    //TODO
                }
            }
            if ($class->isChangeTrackingDeferredExplicit()) {
1338 1339 1340 1341 1342
                //TODO
            }
        }

        if ($prevManagedCopy !== null) {
romanb's avatar
romanb committed
1343
            $assocField = $assoc->sourceFieldName;
1344 1345
            $prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy));
            if ($assoc->isOneToOne()) {
1346
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1347
            } else {
1348
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->hydrateAdd($managedCopy);
1349 1350 1351 1352 1353 1354 1355
            }
        }

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

        return $managedCopy;
    }
romanb's avatar
romanb committed
1356
    
1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399
    /**
     * 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],
                        $this->_entityStates[$oid]);
                break;
            case self::STATE_NEW:
            case self::STATE_DETACHED:
                return;
        }
        
        $this->_cascadeDetach($entity, $visited);
    }
    
romanb's avatar
romanb committed
1400
    /**
1401 1402
     * Refreshes the state of the given entity from the database, overwriting
     * any local, unpersisted changes.
romanb's avatar
romanb committed
1403
     * 
1404
     * @param object $entity The entity to refresh.
1405
     * @throws InvalidArgumentException If the entity is not MANAGED.
romanb's avatar
romanb committed
1406 1407 1408 1409
     */
    public function refresh($entity)
    {
        $visited = array();
1410
        $this->_doRefresh($entity, $visited);
romanb's avatar
romanb committed
1411 1412 1413 1414 1415 1416 1417
    }
    
    /**
     * Executes a refresh operation on an entity.
     * 
     * @param object $entity The entity to refresh.
     * @param array $visited The already visited entities during cascades.
1418
     * @throws InvalidArgumentException If the entity is not MANAGED.
romanb's avatar
romanb committed
1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437
     */
    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));
        switch ($this->getEntityState($entity)) {
            case self::STATE_MANAGED:
                $this->getEntityPersister($class->name)->load(
                    array_combine($class->identifier, $this->_entityIdentifiers[$oid]),
                    $entity
                );
                break;
            default:
1438
                throw new \InvalidArgumentException("Entity is not MANAGED.");
romanb's avatar
romanb committed
1439
        }
1440
        
romanb's avatar
romanb committed
1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457
        $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));
        foreach ($class->associationMappings as $assocMapping) {
            if ( ! $assocMapping->isCascadeRefresh) {
                continue;
            }
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]->getValue($entity);
1458
            if ($relatedEntities instanceof ICollection) {
romanb's avatar
romanb committed
1459 1460 1461 1462 1463 1464 1465 1466
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doRefresh($relatedEntity, $visited);
                }
            } else if ($relatedEntities !== null) {
                $this->_doRefresh($relatedEntities, $visited);
            }
        }
    }
1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481
    
    /**
     * 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));
        foreach ($class->associationMappings as $assocMapping) {
            if ( ! $assocMapping->isCascadeDetach) {
                continue;
            }
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]->getValue($entity);
1482
            if ($relatedEntities instanceof ICollection) {
1483 1484 1485 1486 1487 1488 1489 1490
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doDetach($relatedEntity, $visited);
                }
            } else if ($relatedEntities !== null) {
                $this->_doDetach($relatedEntities, $visited);
            }
        }
    }
1491 1492 1493

    /**
     * Cascades a merge operation to associated entities.
1494
     *
1495 1496 1497
     * @param object $entity
     * @param object $managedCopy
     * @param array $visited
1498 1499 1500 1501
     */
    private function _cascadeMerge($entity, $managedCopy, array &$visited)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
1502
        foreach ($class->associationMappings as $assocMapping) {
romanb's avatar
romanb committed
1503
            if ( ! $assocMapping->isCascadeMerge) {
1504 1505
                continue;
            }
1506
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]->getValue($entity);
1507
            if ($relatedEntities instanceof ICollection) {
1508 1509 1510
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doMerge($relatedEntity, $visited, $managedCopy, $assocMapping);
                }
1511
            } else if ($relatedEntities !== null) {
1512 1513 1514 1515 1516
                $this->_doMerge($relatedEntities, $visited, $managedCopy, $assocMapping);
            }
        }
    }

1517 1518 1519
    /**
     * Cascades the save operation to associated entities.
     *
1520
     * @param object $entity
1521
     * @param array $visited
1522
     * @param array $insertNow
1523
     */
romanb's avatar
romanb committed
1524
    private function _cascadePersist($entity, array &$visited)
1525
    {
1526
        $class = $this->_em->getClassMetadata(get_class($entity));
1527
        foreach ($class->associationMappings as $assocMapping) {
1528
            if ( ! $assocMapping->isCascadePersist) {
1529 1530
                continue;
            }
romanb's avatar
romanb committed
1531
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]->getValue($entity);
1532
            if (($relatedEntities instanceof ICollection || is_array($relatedEntities))) {
1533
                foreach ($relatedEntities as $relatedEntity) {
romanb's avatar
romanb committed
1534
                    $this->_doPersist($relatedEntity, $visited);
1535
                }
1536
            } else if ($relatedEntities !== null) {
romanb's avatar
romanb committed
1537
                $this->_doPersist($relatedEntities, $visited);
1538 1539 1540 1541
            }
        }
    }

romanb's avatar
romanb committed
1542 1543 1544
    /**
     * Cascades the delete operation to associated entities.
     *
1545
     * @param object $entity
1546
     * @param array $visited
romanb's avatar
romanb committed
1547
     */
romanb's avatar
romanb committed
1548
    private function _cascadeRemove($entity, array &$visited)
1549
    {
romanb's avatar
romanb committed
1550
        $class = $this->_em->getClassMetadata(get_class($entity));
1551
        foreach ($class->associationMappings as $assocMapping) {
1552
            if ( ! $assocMapping->isCascadeRemove) {
romanb's avatar
romanb committed
1553 1554
                continue;
            }
romanb's avatar
romanb committed
1555
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]
romanb's avatar
romanb committed
1556
                    ->getValue($entity);
1557
            if ($relatedEntities instanceof ICollection || is_array($relatedEntities)) {
romanb's avatar
romanb committed
1558
                foreach ($relatedEntities as $relatedEntity) {
romanb's avatar
romanb committed
1559
                    $this->_doRemove($relatedEntity, $visited);
romanb's avatar
romanb committed
1560
                }
1561
            } else if ($relatedEntities !== null) {
romanb's avatar
romanb committed
1562
                $this->_doRemove($relatedEntities, $visited);
romanb's avatar
romanb committed
1563 1564
            }
        }
1565
    }
romanb's avatar
romanb committed
1566 1567 1568 1569 1570 1571

    /**
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
     *
     * @return Doctrine\ORM\Internal\CommitOrderCalculator
     */
1572 1573 1574 1575 1576
    public function getCommitOrderCalculator()
    {
        return $this->_commitOrderCalculator;
    }

romanb's avatar
romanb committed
1577
    /**
1578
     * Clears the UnitOfWork.
romanb's avatar
romanb committed
1579
     */
1580
    public function clear()
1581
    {
romanb's avatar
romanb committed
1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594
        $this->_identityMap =
        $this->_entityIdentifiers =
        $this->_originalEntityData =
        $this->_entityChangeSets =
        $this->_entityStates =
        $this->_scheduledForDirtyCheck =
        $this->_entityInsertions =
        $this->_entityUpdates =
        $this->_entityDeletions =
        $this->_collectionDeletions =
        //$this->_collectionCreations =
        $this->_collectionUpdates =
        $this->_orphanRemovals = array();
1595 1596
        $this->_commitOrderCalculator->clear();
    }
1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609
    
    /**
     * 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.
     * 
     * @param object $entity
     */
    public function scheduleOrphanRemoval($entity)
    {
        $this->_orphanRemovals[spl_object_hash($entity)] = $entity;
    }
1610

1611
    /*public function scheduleCollectionUpdate(PersistentCollection $coll)
romanb's avatar
romanb committed
1612 1613
    {
        $this->_collectionUpdates[] = $coll;
1614
    }*/
1615

1616
    /*public function isCollectionScheduledForUpdate(PersistentCollection $coll)
romanb's avatar
romanb committed
1617 1618
    {
        //...
1619 1620 1621 1622 1623 1624 1625 1626
    }*/
    
    /**
     * INTERNAL:
     * Schedules a complete collection for removal when this UnitOfWork commits.
     *
     * @param PersistentCollection $coll
     */
1627
    public function scheduleCollectionDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1628 1629 1630 1631 1632
    {
        //TODO: if $coll is already scheduled for recreation ... what to do?
        // Just remove $coll from the scheduled recreations?
        $this->_collectionDeletions[] = $coll;
    }
1633

1634
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1635 1636 1637
    {
        //...
    }
1638

1639
    /*public function scheduleCollectionRecreation(PersistentCollection $coll)
romanb's avatar
romanb committed
1640 1641
    {
        $this->_collectionRecreations[] = $coll;
1642
    }*/
1643

1644
    /*public function isCollectionScheduledForRecreation(PersistentCollection $coll)
romanb's avatar
romanb committed
1645 1646
    {
        //...
1647
    }*/
1648

1649
    /**
1650
     * INTERNAL:
1651
     * Creates an entity. Used for reconstitution of entities during hydration.
1652
     *
1653 1654
     * @param string $className  The name of the entity class.
     * @param array $data  The data for the entity.
1655 1656
     * @return object The created entity instance.
     * @internal Highly performance-sensitive method. Run the performance test suites when
1657
     *           making modifications.
1658
     */
1659
    public function createEntity($className, array $data, $hints = array())
1660
    {
1661
        $class = $this->_em->getClassMetadata($className);
1662

1663 1664 1665
        if ($class->isIdentifierComposite) {
            $id = array();
            foreach ($class->identifier as $fieldName) {
1666 1667
                $id[] = $data[$fieldName];
            }
1668
            $idHash = implode(' ', $id);
1669
        } else {
1670
            $id = array($data[$class->identifier[0]]);
1671 1672
            $idHash = $id[0];
        }
1673 1674 1675

        if (isset($this->_identityMap[$class->rootEntityName][$idHash])) {
            $entity = $this->_identityMap[$class->rootEntityName][$idHash];
1676
            $oid = spl_object_hash($entity);
1677
            $overrideLocalChanges = isset($hints[Query::HINT_REFRESH]);
1678 1679
        } else {
            $entity = new $className;
1680 1681
            $oid = spl_object_hash($entity);
            $this->_entityIdentifiers[$oid] = $id;
1682
            $this->_entityStates[$oid] = self::STATE_MANAGED;
1683
            $this->_originalEntityData[$oid] = $data;
1684 1685 1686 1687
            $this->_identityMap[$class->rootEntityName][$idHash] = $entity;
            if ($entity instanceof \Doctrine\Common\NotifyPropertyChanged) {
                $entity->addPropertyChangedListener($this);
            }
1688
            $overrideLocalChanges = true;
1689 1690 1691
        }

        if ($overrideLocalChanges) {
1692 1693 1694 1695 1696 1697 1698
            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);
                    }
1699
                }
1700 1701 1702
            }
        } else {
            foreach ($data as $field => $value) {
1703 1704
                if (isset($class->reflFields[$field])) {
                    $currentValue = $class->reflFields[$field]->getValue($entity);
romanb's avatar
romanb committed
1705 1706 1707 1708
                    // Only override the current value if:
                    // a) There was no original value yet (nothing in _originalEntityData)
                    // or
                    // b) The original value is the same as the current value (it was not changed).
1709
                    if ( ! isset($this->_originalEntityData[$oid][$field]) ||
1710
                            $currentValue == $this->_originalEntityData[$oid][$field]) {
1711
                        $class->reflFields[$field]->setValue($entity, $value);
1712
                    }
1713 1714 1715
                }
            }
        }
1716
        
1717
        if (isset($class->lifecycleCallbacks[Events::postLoad])) {
1718 1719 1720
            $class->invokeLifecycleCallbacks(Events::postLoad, $entity);
        }
        if ($this->_evm->hasListeners(Events::postLoad)) {
1721
            $this->_evm->dispatchEvent(Events::postLoad, new LifecycleEventArgs($entity));
1722
        }
1723
        
1724 1725

        return $entity;
1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736
    }

    /**
     * Gets the identity map of the UnitOfWork.
     *
     * @return array
     */
    public function getIdentityMap()
    {
        return $this->_identityMap;
    }
1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755

    /**
     * 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();
    }

    /**
     * INTERNAL:
1756
     * Sets a property value of the original data array of an entity.
1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768
     *
     * @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.
1769
     * The returned value is always an array of identifier values. If the entity
1770
     * has a composite identifier then the identifier values are in the same
1771
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
1772 1773 1774 1775 1776 1777 1778 1779
     *
     * @param object $entity
     * @return array The identifier values.
     */
    public function getEntityIdentifier($entity)
    {
        return $this->_entityIdentifiers[spl_object_hash($entity)];
    }
1780 1781

    /**
1782 1783
     * Tries to find an entity with the given identifier in the identity map of
     * this UnitOfWork.
1784
     *
1785 1786 1787 1788
     * @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.
1789 1790 1791
     */
    public function tryGetById($id, $rootClassName)
    {
1792
        $idHash = implode(' ', (array)$id);
1793 1794 1795 1796 1797
        if (isset($this->_identityMap[$rootClassName][$idHash])) {
            return $this->_identityMap[$rootClassName][$idHash];
        }
        return false;
    }
1798

1799 1800 1801 1802 1803
    /**
     * Schedules an entity for dirty-checking at commit-time.
     *
     * @param object $entity The entity to schedule for dirty-checking.
     */
1804 1805
    public function scheduleForDirtyCheck($entity)
    {
1806
        $rootClassName = $this->_em->getClassMetadata(get_class($entity))->rootEntityName;
1807 1808 1809
        $this->_scheduledForDirtyCheck[$rootClassName] = $entity;
    }

1810 1811 1812 1813 1814 1815 1816 1817 1818 1819
    /**
     * 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);
    }

1820 1821 1822
    /**
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
     * number of entities in the identity map.
1823 1824
     *
     * @return integer
1825 1826 1827 1828
     */
    public function size()
    {
        $count = 0;
romanb's avatar
romanb committed
1829 1830 1831
        foreach ($this->_identityMap as $entitySet) {
            $count += count($entitySet);
        }
1832 1833
        return $count;
    }
1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844

    /**
     * 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);
1845
            if ($class->isInheritanceTypeNone()) {
1846
                $persister = new Persisters\StandardEntityPersister($this->_em, $class);
1847 1848 1849
            } else if ($class->isInheritanceTypeSingleTable()) {
                $persister = new Persisters\SingleTablePersister($this->_em, $class);
            } else if ($class->isInheritanceTypeJoined()) {
1850
                $persister = new Persisters\JoinedSubclassPersister($this->_em, $class);
1851
            } else {
1852
                $persister = new Persisters\UnionSubclassPersister($this->_em, $class);
1853 1854 1855 1856 1857 1858
            }
            $this->_persisters[$entityName] = $persister;
        }
        return $this->_persisters[$entityName];
    }

1859 1860 1861 1862 1863 1864
    /**
     * Gets a collection persister for a collection-valued association.
     *
     * @param AssociationMapping $association
     * @return AbstractCollectionPersister
     */
1865 1866 1867 1868
    public function getCollectionPersister($association)
    {
        $type = get_class($association);
        if ( ! isset($this->_collectionPersisters[$type])) {
1869 1870 1871 1872
            if ($association instanceof Mapping\OneToManyMapping) {
                $persister = new Persisters\OneToManyPersister($this->_em);
            } else if ($association instanceof Mapping\ManyToManyMapping) {
                $persister = new Persisters\ManyToManyPersister($this->_em);
1873 1874 1875 1876 1877
            }
            $this->_collectionPersisters[$type] = $persister;
        }
        return $this->_collectionPersisters[$type];
    }
1878

1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894
    /**
     * INTERNAL:
     * Registers an entity as managed.
     *
     * @param object $entity The entity.
     * @param array $id The identifier values.
     * @param array $data The original entity data.
     */
    public function registerManaged($entity, $id, $data)
    {
        $oid = spl_object_hash($entity);
        $this->_entityIdentifiers[$oid] = $id;
        $this->_entityStates[$oid] = self::STATE_MANAGED;
        $this->_originalEntityData[$oid] = $data;
        $this->addToIdentityMap($entity);
    }
1895

1896 1897 1898 1899 1900 1901 1902 1903
    /**
     * 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
1904
        unset($this->_entityChangeSets[$oid]);
1905
    }
1906

1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918
    /* 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)
    {
1919 1920
        $oid = spl_object_hash($entity);
        $class = $this->_em->getClassMetadata(get_class($entity));
1921

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

1924 1925 1926
        if (isset($class->associationMappings[$propertyName])) {
            $assoc = $class->associationMappings[$propertyName];
            if ($assoc->isOneToOne() && $assoc->isOwningSide) {
1927
                $this->_entityUpdates[$oid] = $entity;
1928 1929 1930 1931 1932
            } else if ($oldValue instanceof PersistentCollection) {
                // A PersistentCollection was de-referenced, so delete it.
                if  ( ! in_array($oldValue, $this->_collectionDeletions, true)) {
                    $this->_collectionDeletions[] = $oldValue;
                }
1933
            }
1934 1935
        } else {
            $this->_entityUpdates[$oid] = $entity;
1936
        }
1937
    }
1938
}