PageRenderTime 321ms CodeModel.GetById 141ms app.highlight 109ms RepoModel.GetById 52ms app.codeStats 1ms

/protected/extensions/doctrine/vendors/Doctrine/ORM/UnitOfWork.php

https://bitbucket.org/NordLabs/yiidoctrine
PHP | 2949 lines | 1581 code | 498 blank | 870 comment | 306 complexity | 83d7e3d31b0e464f220dd78603ce6d3e MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2/*
   3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
   5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
   6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
   7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14 *
  15 * This software consists of voluntary contributions made by many individuals
  16 * and is licensed under the LGPL. For more information, see
  17 * <http://www.doctrine-project.org>.
  18 */
  19
  20namespace Doctrine\ORM;
  21
  22use Exception, InvalidArgumentException, UnexpectedValueException,
  23    Doctrine\Common\Collections\ArrayCollection,
  24    Doctrine\Common\Collections\Collection,
  25    Doctrine\Common\NotifyPropertyChanged,
  26    Doctrine\Common\PropertyChangedListener,
  27    Doctrine\Common\Persistence\ObjectManagerAware,
  28    Doctrine\ORM\Event\LifecycleEventArgs,
  29    Doctrine\ORM\Mapping\ClassMetadata,
  30    Doctrine\ORM\Proxy\Proxy;
  31
  32/**
  33 * The UnitOfWork is responsible for tracking changes to objects during an
  34 * "object-level" transaction and for writing out changes to the database
  35 * in the correct order.
  36 *
  37 * @since       2.0
  38 * @author      Benjamin Eberlei <kontakt@beberlei.de>
  39 * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
  40 * @author      Jonathan Wage <jonwage@gmail.com>
  41 * @author      Roman Borschel <roman@code-factory.org>
  42 * @internal    This class contains highly performance-sensitive code.
  43 */
  44class UnitOfWork implements PropertyChangedListener
  45{
  46    /**
  47     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
  48     */
  49    const STATE_MANAGED = 1;
  50
  51    /**
  52     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
  53     * and is not (yet) managed by an EntityManager.
  54     */
  55    const STATE_NEW = 2;
  56
  57    /**
  58     * A detached entity is an instance with persistent state and identity that is not
  59     * (or no longer) associated with an EntityManager (and a UnitOfWork).
  60     */
  61    const STATE_DETACHED = 3;
  62
  63    /**
  64     * A removed entity instance is an instance with a persistent identity,
  65     * associated with an EntityManager, whose persistent state will be deleted
  66     * on commit.
  67     */
  68    const STATE_REMOVED = 4;
  69
  70    /**
  71     * The identity map that holds references to all managed entities that have
  72     * an identity. The entities are grouped by their class name.
  73     * Since all classes in a hierarchy must share the same identifier set,
  74     * we always take the root class name of the hierarchy.
  75     *
  76     * @var array
  77     */
  78    private $identityMap = array();
  79
  80    /**
  81     * Map of all identifiers of managed entities.
  82     * Keys are object ids (spl_object_hash).
  83     *
  84     * @var array
  85     */
  86    private $entityIdentifiers = array();
  87
  88    /**
  89     * Map of the original entity data of managed entities.
  90     * Keys are object ids (spl_object_hash). This is used for calculating changesets
  91     * at commit time.
  92     *
  93     * @var array
  94     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
  95     *           A value will only really be copied if the value in the entity is modified
  96     *           by the user.
  97     */
  98    private $originalEntityData = array();
  99
 100    /**
 101     * Map of entity changes. Keys are object ids (spl_object_hash).
 102     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
 103     *
 104     * @var array
 105     */
 106    private $entityChangeSets = array();
 107
 108    /**
 109     * The (cached) states of any known entities.
 110     * Keys are object ids (spl_object_hash).
 111     *
 112     * @var array
 113     */
 114    private $entityStates = array();
 115
 116    /**
 117     * Map of entities that are scheduled for dirty checking at commit time.
 118     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
 119     * Keys are object ids (spl_object_hash).
 120     *
 121     * @var array
 122     * @todo rename: scheduledForSynchronization
 123     */
 124    private $scheduledForDirtyCheck = array();
 125
 126    /**
 127     * A list of all pending entity insertions.
 128     *
 129     * @var array
 130     */
 131    private $entityInsertions = array();
 132
 133    /**
 134     * A list of all pending entity updates.
 135     *
 136     * @var array
 137     */
 138    private $entityUpdates = array();
 139
 140    /**
 141     * Any pending extra updates that have been scheduled by persisters.
 142     *
 143     * @var array
 144     */
 145    private $extraUpdates = array();
 146
 147    /**
 148     * A list of all pending entity deletions.
 149     *
 150     * @var array
 151     */
 152    private $entityDeletions = array();
 153
 154    /**
 155     * All pending collection deletions.
 156     *
 157     * @var array
 158     */
 159    private $collectionDeletions = array();
 160
 161    /**
 162     * All pending collection updates.
 163     *
 164     * @var array
 165     */
 166    private $collectionUpdates = array();
 167
 168    /**
 169     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
 170     * At the end of the UnitOfWork all these collections will make new snapshots
 171     * of their data.
 172     *
 173     * @var array
 174     */
 175    private $visitedCollections = array();
 176
 177    /**
 178     * The EntityManager that "owns" this UnitOfWork instance.
 179     *
 180     * @var \Doctrine\ORM\EntityManager
 181     */
 182    private $em;
 183
 184    /**
 185     * The calculator used to calculate the order in which changes to
 186     * entities need to be written to the database.
 187     *
 188     * @var \Doctrine\ORM\Internal\CommitOrderCalculator
 189     */
 190    private $commitOrderCalculator;
 191
 192    /**
 193     * The entity persister instances used to persist entity instances.
 194     *
 195     * @var array
 196     */
 197    private $persisters = array();
 198
 199    /**
 200     * The collection persister instances used to persist collections.
 201     *
 202     * @var array
 203     */
 204    private $collectionPersisters = array();
 205
 206    /**
 207     * The EventManager used for dispatching events.
 208     *
 209     * @var EventManager
 210     */
 211    private $evm;
 212
 213    /**
 214     * Orphaned entities that are scheduled for removal.
 215     *
 216     * @var array
 217     */
 218    private $orphanRemovals = array();
 219
 220    /**
 221     * Read-Only objects are never evaluated
 222     *
 223     * @var array
 224     */
 225    private $readOnlyObjects = array();
 226
 227    /**
 228     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
 229     *
 230     * @var array
 231     */
 232    private $eagerLoadingEntities = array();
 233
 234    /**
 235     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
 236     *
 237     * @param \Doctrine\ORM\EntityManager $em
 238     */
 239    public function __construct(EntityManager $em)
 240    {
 241        $this->em = $em;
 242        $this->evm = $em->getEventManager();
 243    }
 244
 245    /**
 246     * Commits the UnitOfWork, executing all operations that have been postponed
 247     * up to this point. The state of all managed entities will be synchronized with
 248     * the database.
 249     *
 250     * The operations are executed in the following order:
 251     *
 252     * 1) All entity insertions
 253     * 2) All entity updates
 254     * 3) All collection deletions
 255     * 4) All collection updates
 256     * 5) All entity deletions
 257     *
 258     * @param object $entity
 259     * @return void
 260     */
 261    public function commit($entity = null)
 262    {
 263        // Raise preFlush
 264        if ($this->evm->hasListeners(Events::preFlush)) {
 265            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->em));
 266        }
 267
 268        // Compute changes done since last commit.
 269        if ($entity === null) {
 270            $this->computeChangeSets();
 271        } else {
 272            $this->computeSingleEntityChangeSet($entity);
 273        }
 274
 275        if ( ! ($this->entityInsertions ||
 276                $this->entityDeletions ||
 277                $this->entityUpdates ||
 278                $this->collectionUpdates ||
 279                $this->collectionDeletions ||
 280                $this->orphanRemovals)) {
 281            return; // Nothing to do.
 282        }
 283
 284        if ($this->orphanRemovals) {
 285            foreach ($this->orphanRemovals as $orphan) {
 286                $this->remove($orphan);
 287            }
 288        }
 289
 290        // Raise onFlush
 291        if ($this->evm->hasListeners(Events::onFlush)) {
 292            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->em));
 293        }
 294
 295        // Now we need a commit order to maintain referential integrity
 296        $commitOrder = $this->getCommitOrder();
 297
 298        $conn = $this->em->getConnection();
 299        $conn->beginTransaction();
 300
 301        try {
 302            if ($this->entityInsertions) {
 303                foreach ($commitOrder as $class) {
 304                    $this->executeInserts($class);
 305                }
 306            }
 307
 308            if ($this->entityUpdates) {
 309                foreach ($commitOrder as $class) {
 310                    $this->executeUpdates($class);
 311                }
 312            }
 313
 314            // Extra updates that were requested by persisters.
 315            if ($this->extraUpdates) {
 316                $this->executeExtraUpdates();
 317            }
 318
 319            // Collection deletions (deletions of complete collections)
 320            foreach ($this->collectionDeletions as $collectionToDelete) {
 321                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
 322            }
 323            // Collection updates (deleteRows, updateRows, insertRows)
 324            foreach ($this->collectionUpdates as $collectionToUpdate) {
 325                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
 326            }
 327
 328            // Entity deletions come last and need to be in reverse commit order
 329            if ($this->entityDeletions) {
 330                for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
 331                    $this->executeDeletions($commitOrder[$i]);
 332                }
 333            }
 334
 335            $conn->commit();
 336        } catch (Exception $e) {
 337            $this->em->close();
 338            $conn->rollback();
 339
 340            throw $e;
 341        }
 342
 343        // Take new snapshots from visited collections
 344        foreach ($this->visitedCollections as $coll) {
 345            $coll->takeSnapshot();
 346        }
 347
 348        // Raise postFlush
 349        if ($this->evm->hasListeners(Events::postFlush)) {
 350            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->em));
 351        }
 352
 353        // Clear up
 354        $this->entityInsertions =
 355        $this->entityUpdates =
 356        $this->entityDeletions =
 357        $this->extraUpdates =
 358        $this->entityChangeSets =
 359        $this->collectionUpdates =
 360        $this->collectionDeletions =
 361        $this->visitedCollections =
 362        $this->scheduledForDirtyCheck =
 363        $this->orphanRemovals = array();
 364    }
 365
 366    /**
 367     * Compute the changesets of all entities scheduled for insertion
 368     *
 369     * @return void
 370     */
 371    private function computeScheduleInsertsChangeSets()
 372    {
 373        foreach ($this->entityInsertions as $entity) {
 374            $class = $this->em->getClassMetadata(get_class($entity));
 375
 376            $this->computeChangeSet($class, $entity);
 377        }
 378    }
 379
 380    /**
 381     * Only flush the given entity according to a ruleset that keeps the UoW consistent.
 382     *
 383     * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
 384     * 2. Read Only entities are skipped.
 385     * 3. Proxies are skipped.
 386     * 4. Only if entity is properly managed.
 387     *
 388     * @param  object $entity
 389     * @return void
 390     */
 391    private function computeSingleEntityChangeSet($entity)
 392    {
 393        if ( $this->getEntityState($entity) !== self::STATE_MANAGED) {
 394            throw new \InvalidArgumentException("Entity has to be managed for single computation " . self::objToStr($entity));
 395        }
 396
 397        $class = $this->em->getClassMetadata(get_class($entity));
 398
 399        if ($class->isChangeTrackingDeferredImplicit()) {
 400            $this->persist($entity);
 401        }
 402
 403        // Compute changes for INSERTed entities first. This must always happen even in this case.
 404        $this->computeScheduleInsertsChangeSets();
 405
 406        if ($class->isReadOnly) {
 407            return;
 408        }
 409
 410        // Ignore uninitialized proxy objects
 411        if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
 412            return;
 413        }
 414
 415        // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
 416        $oid = spl_object_hash($entity);
 417
 418        if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) {
 419            $this->computeChangeSet($class, $entity);
 420        }
 421    }
 422
 423    /**
 424     * Executes any extra updates that have been scheduled.
 425     */
 426    private function executeExtraUpdates()
 427    {
 428        foreach ($this->extraUpdates as $oid => $update) {
 429            list ($entity, $changeset) = $update;
 430
 431            $this->entityChangeSets[$oid] = $changeset;
 432            $this->getEntityPersister(get_class($entity))->update($entity);
 433        }
 434    }
 435
 436    /**
 437     * Gets the changeset for an entity.
 438     *
 439     * @return array
 440     */
 441    public function getEntityChangeSet($entity)
 442    {
 443        $oid = spl_object_hash($entity);
 444
 445        if (isset($this->entityChangeSets[$oid])) {
 446            return $this->entityChangeSets[$oid];
 447        }
 448
 449        return array();
 450    }
 451
 452    /**
 453     * Computes the changes that happened to a single entity.
 454     *
 455     * Modifies/populates the following properties:
 456     *
 457     * {@link _originalEntityData}
 458     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
 459     * then it was not fetched from the database and therefore we have no original
 460     * entity data yet. All of the current entity data is stored as the original entity data.
 461     *
 462     * {@link _entityChangeSets}
 463     * The changes detected on all properties of the entity are stored there.
 464     * A change is a tuple array where the first entry is the old value and the second
 465     * entry is the new value of the property. Changesets are used by persisters
 466     * to INSERT/UPDATE the persistent entity state.
 467     *
 468     * {@link _entityUpdates}
 469     * If the entity is already fully MANAGED (has been fetched from the database before)
 470     * and any changes to its properties are detected, then a reference to the entity is stored
 471     * there to mark it for an update.
 472     *
 473     * {@link _collectionDeletions}
 474     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
 475     * then this collection is marked for deletion.
 476     *
 477     * @ignore
 478     * @internal Don't call from the outside.
 479     * @param ClassMetadata $class The class descriptor of the entity.
 480     * @param object $entity The entity for which to compute the changes.
 481     */
 482    public function computeChangeSet(ClassMetadata $class, $entity)
 483    {
 484        $oid = spl_object_hash($entity);
 485
 486        if (isset($this->readOnlyObjects[$oid])) {
 487            return;
 488        }
 489
 490        if ( ! $class->isInheritanceTypeNone()) {
 491            $class = $this->em->getClassMetadata(get_class($entity));
 492        }
 493
 494        // Fire PreFlush lifecycle callbacks
 495        if (isset($class->lifecycleCallbacks[Events::preFlush])) {
 496            $class->invokeLifecycleCallbacks(Events::preFlush, $entity);
 497        }
 498
 499        $actualData = array();
 500
 501        foreach ($class->reflFields as $name => $refProp) {
 502            $value = $refProp->getValue($entity);
 503
 504            if ($class->isCollectionValuedAssociation($name) && $value !== null && ! ($value instanceof PersistentCollection)) {
 505                // If $value is not a Collection then use an ArrayCollection.
 506                if ( ! $value instanceof Collection) {
 507                    $value = new ArrayCollection($value);
 508                }
 509
 510                $assoc = $class->associationMappings[$name];
 511
 512                // Inject PersistentCollection
 513                $value = new PersistentCollection(
 514                    $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value
 515                );
 516                $value->setOwner($entity, $assoc);
 517                $value->setDirty( ! $value->isEmpty());
 518
 519                $class->reflFields[$name]->setValue($entity, $value);
 520
 521                $actualData[$name] = $value;
 522
 523                continue;
 524            }
 525
 526            if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
 527                $actualData[$name] = $value;
 528            }
 529        }
 530
 531        if ( ! isset($this->originalEntityData[$oid])) {
 532            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
 533            // These result in an INSERT.
 534            $this->originalEntityData[$oid] = $actualData;
 535            $changeSet = array();
 536
 537            foreach ($actualData as $propName => $actualValue) {
 538                if ( ! isset($class->associationMappings[$propName])) {
 539                    $changeSet[$propName] = array(null, $actualValue);
 540
 541                    continue;
 542                }
 543
 544                $assoc = $class->associationMappings[$propName];
 545
 546                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
 547                    $changeSet[$propName] = array(null, $actualValue);
 548                }
 549            }
 550
 551            $this->entityChangeSets[$oid] = $changeSet;
 552        } else {
 553            // Entity is "fully" MANAGED: it was already fully persisted before
 554            // and we have a copy of the original data
 555            $originalData           = $this->originalEntityData[$oid];
 556            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
 557            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
 558                ? $this->entityChangeSets[$oid]
 559                : array();
 560
 561            foreach ($actualData as $propName => $actualValue) {
 562                // skip field, its a partially omitted one!
 563                if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
 564                    continue;
 565                }
 566
 567                $orgValue = $originalData[$propName];
 568
 569                // skip if value havent changed
 570                if ($orgValue === $actualValue) {
 571                    continue;
 572                }
 573
 574                // if regular field
 575                if ( ! isset($class->associationMappings[$propName])) {
 576                    if ($isChangeTrackingNotify) {
 577                        continue;
 578                    }
 579
 580                    $changeSet[$propName] = array($orgValue, $actualValue);
 581
 582                    continue;
 583                }
 584
 585                $assoc = $class->associationMappings[$propName];
 586
 587                // Persistent collection was exchanged with the "originally"
 588                // created one. This can only mean it was cloned and replaced
 589                // on another entity.
 590                if ($actualValue instanceof PersistentCollection) {
 591                    $owner = $actualValue->getOwner();
 592                    if ($owner === null) { // cloned
 593                        $actualValue->setOwner($entity, $assoc);
 594                    } else if ($owner !== $entity) { // no clone, we have to fix
 595                        if (!$actualValue->isInitialized()) {
 596                            $actualValue->initialize(); // we have to do this otherwise the cols share state
 597                        }
 598                        $newValue = clone $actualValue;
 599                        $newValue->setOwner($entity, $assoc);
 600                        $class->reflFields[$propName]->setValue($entity, $newValue);
 601                    }
 602                }
 603
 604                if ($orgValue instanceof PersistentCollection) {
 605                    // A PersistentCollection was de-referenced, so delete it.
 606                    $coid = spl_object_hash($orgValue);
 607
 608                    if (isset($this->collectionDeletions[$coid])) {
 609                        continue;
 610                    }
 611
 612                    $this->collectionDeletions[$coid] = $orgValue;
 613                    $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
 614
 615                    continue;
 616                }
 617
 618                if ($assoc['type'] & ClassMetadata::TO_ONE) {
 619                    if ($assoc['isOwningSide']) {
 620                        $changeSet[$propName] = array($orgValue, $actualValue);
 621                    }
 622
 623                    if ($orgValue !== null && $assoc['orphanRemoval']) {
 624                        $this->scheduleOrphanRemoval($orgValue);
 625                    }
 626                }
 627            }
 628
 629            if ($changeSet) {
 630                $this->entityChangeSets[$oid]   = $changeSet;
 631                $this->originalEntityData[$oid] = $actualData;
 632                $this->entityUpdates[$oid]      = $entity;
 633            }
 634        }
 635
 636        // Look for changes in associations of the entity
 637        foreach ($class->associationMappings as $field => $assoc) {
 638            if (($val = $class->reflFields[$field]->getValue($entity)) !== null) {
 639                $this->computeAssociationChanges($assoc, $val);
 640                if (!isset($this->entityChangeSets[$oid]) &&
 641                    $assoc['isOwningSide'] &&
 642                    $assoc['type'] == ClassMetadata::MANY_TO_MANY &&
 643                    $val instanceof PersistentCollection &&
 644                    $val->isDirty()) {
 645                    $this->entityChangeSets[$oid]   = array();
 646                    $this->originalEntityData[$oid] = $actualData;
 647                    $this->entityUpdates[$oid]      = $entity;
 648                }
 649            }
 650        }
 651    }
 652
 653    /**
 654     * Computes all the changes that have been done to entities and collections
 655     * since the last commit and stores these changes in the _entityChangeSet map
 656     * temporarily for access by the persisters, until the UoW commit is finished.
 657     */
 658    public function computeChangeSets()
 659    {
 660        // Compute changes for INSERTed entities first. This must always happen.
 661        $this->computeScheduleInsertsChangeSets();
 662
 663        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
 664        foreach ($this->identityMap as $className => $entities) {
 665            $class = $this->em->getClassMetadata($className);
 666
 667            // Skip class if instances are read-only
 668            if ($class->isReadOnly) {
 669                continue;
 670            }
 671
 672            // If change tracking is explicit or happens through notification, then only compute
 673            // changes on entities of that type that are explicitly marked for synchronization.
 674            switch (true) {
 675                case ($class->isChangeTrackingDeferredImplicit()):
 676                    $entitiesToProcess = $entities;
 677                    break;
 678
 679                case (isset($this->scheduledForDirtyCheck[$className])):
 680                    $entitiesToProcess = $this->scheduledForDirtyCheck[$className];
 681                    break;
 682
 683                default:
 684                    $entitiesToProcess = array();
 685
 686            }
 687
 688            foreach ($entitiesToProcess as $entity) {
 689                // Ignore uninitialized proxy objects
 690                if ($entity instanceof Proxy && ! $entity->__isInitialized__) {
 691                    continue;
 692                }
 693
 694                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here.
 695                $oid = spl_object_hash($entity);
 696
 697                if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) {
 698                    $this->computeChangeSet($class, $entity);
 699                }
 700            }
 701        }
 702    }
 703
 704    /**
 705     * Computes the changes of an association.
 706     *
 707     * @param AssociationMapping $assoc
 708     * @param mixed $value The value of the association.
 709     */
 710    private function computeAssociationChanges($assoc, $value)
 711    {
 712        if ($value instanceof Proxy && ! $value->__isInitialized__) {
 713            return;
 714        }
 715
 716        if ($value instanceof PersistentCollection && $value->isDirty()) {
 717            $coid = spl_object_hash($value);
 718
 719            if ($assoc['isOwningSide']) {
 720                $this->collectionUpdates[$coid] = $value;
 721            }
 722
 723            $this->visitedCollections[$coid] = $value;
 724        }
 725
 726        // Look through the entities, and in any of their associations,
 727        // for transient (new) entities, recursively. ("Persistence by reachability")
 728        // Unwrap. Uninitialized collections will simply be empty.
 729        $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? array($value) : $value->unwrap();
 730        $targetClass    = $this->em->getClassMetadata($assoc['targetEntity']);
 731
 732        foreach ($unwrappedValue as $key => $entry) {
 733            $state = $this->getEntityState($entry, self::STATE_NEW);
 734            $oid   = spl_object_hash($entry);
 735
 736            if (!($entry instanceof $assoc['targetEntity'])) {
 737                throw new ORMException(sprintf("Found entity of type %s on association %s#%s, but expecting %s",
 738                    get_class($entry),
 739                    $assoc['sourceEntity'],
 740                    $assoc['fieldName'],
 741                    $targetClass->name
 742                ));
 743            }
 744
 745            switch ($state) {
 746                case self::STATE_NEW:
 747                    if ( ! $assoc['isCascadePersist']) {
 748                        throw ORMInvalidArgumentException::newEntityFoundThroughRelationship($assoc, $entry);
 749                    }
 750
 751                    $this->persistNew($targetClass, $entry);
 752                    $this->computeChangeSet($targetClass, $entry);
 753                    break;
 754
 755                case self::STATE_REMOVED:
 756                    // Consume the $value as array (it's either an array or an ArrayAccess)
 757                    // and remove the element from Collection.
 758                    if ($assoc['type'] & ClassMetadata::TO_MANY) {
 759                        unset($value[$key]);
 760                    }
 761                    break;
 762
 763                case self::STATE_DETACHED:
 764                    // Can actually not happen right now as we assume STATE_NEW,
 765                    // so the exception will be raised from the DBAL layer (constraint violation).
 766                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
 767                    break;
 768
 769                default:
 770                    // MANAGED associated entities are already taken into account
 771                    // during changeset calculation anyway, since they are in the identity map.
 772            }
 773        }
 774    }
 775
 776    private function persistNew($class, $entity)
 777    {
 778        $oid = spl_object_hash($entity);
 779
 780        if (isset($class->lifecycleCallbacks[Events::prePersist])) {
 781            $class->invokeLifecycleCallbacks(Events::prePersist, $entity);
 782        }
 783
 784        if ($this->evm->hasListeners(Events::prePersist)) {
 785            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($entity, $this->em));
 786        }
 787
 788        $idGen = $class->idGenerator;
 789
 790        if ( ! $idGen->isPostInsertGenerator()) {
 791            $idValue = $idGen->generate($this->em, $entity);
 792
 793            if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) {
 794                $idValue = array($class->identifier[0] => $idValue);
 795
 796                $class->setIdentifierValues($entity, $idValue);
 797            }
 798
 799            $this->entityIdentifiers[$oid] = $idValue;
 800        }
 801
 802        $this->entityStates[$oid] = self::STATE_MANAGED;
 803
 804        $this->scheduleForInsert($entity);
 805    }
 806
 807    /**
 808     * INTERNAL:
 809     * Computes the changeset of an individual entity, independently of the
 810     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
 811     *
 812     * The passed entity must be a managed entity. If the entity already has a change set
 813     * because this method is invoked during a commit cycle then the change sets are added.
 814     * whereby changes detected in this method prevail.
 815     *
 816     * @ignore
 817     * @param ClassMetadata $class The class descriptor of the entity.
 818     * @param object $entity The entity for which to (re)calculate the change set.
 819     * @throws InvalidArgumentException If the passed entity is not MANAGED.
 820     */
 821    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
 822    {
 823        $oid = spl_object_hash($entity);
 824
 825        if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) {
 826            throw ORMInvalidArgumentException::entityNotManaged($entity);
 827        }
 828
 829        // skip if change tracking is "NOTIFY"
 830        if ($class->isChangeTrackingNotify()) {
 831            return;
 832        }
 833
 834        if ( ! $class->isInheritanceTypeNone()) {
 835            $class = $this->em->getClassMetadata(get_class($entity));
 836        }
 837
 838        $actualData = array();
 839
 840        foreach ($class->reflFields as $name => $refProp) {
 841            if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
 842                $actualData[$name] = $refProp->getValue($entity);
 843            }
 844        }
 845
 846        $originalData = $this->originalEntityData[$oid];
 847        $changeSet = array();
 848
 849        foreach ($actualData as $propName => $actualValue) {
 850            $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
 851
 852            if (is_object($orgValue) && $orgValue !== $actualValue) {
 853                $changeSet[$propName] = array($orgValue, $actualValue);
 854            } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
 855                $changeSet[$propName] = array($orgValue, $actualValue);
 856            }
 857        }
 858
 859        if ($changeSet) {
 860            if (isset($this->entityChangeSets[$oid])) {
 861                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
 862            }
 863
 864            $this->originalEntityData[$oid] = $actualData;
 865        }
 866    }
 867
 868    /**
 869     * Executes all entity insertions for entities of the specified type.
 870     *
 871     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
 872     */
 873    private function executeInserts($class)
 874    {
 875        $className = $class->name;
 876        $persister = $this->getEntityPersister($className);
 877        $entities  = array();
 878
 879        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
 880        $hasListeners          = $this->evm->hasListeners(Events::postPersist);
 881
 882        foreach ($this->entityInsertions as $oid => $entity) {
 883            if (get_class($entity) !== $className) {
 884                continue;
 885            }
 886
 887            $persister->addInsert($entity);
 888
 889            unset($this->entityInsertions[$oid]);
 890
 891            if ($hasLifecycleCallbacks || $hasListeners) {
 892                $entities[] = $entity;
 893            }
 894        }
 895
 896        $postInsertIds = $persister->executeInserts();
 897
 898        if ($postInsertIds) {
 899            // Persister returned post-insert IDs
 900            foreach ($postInsertIds as $id => $entity) {
 901                $oid     = spl_object_hash($entity);
 902                $idField = $class->identifier[0];
 903
 904                $class->reflFields[$idField]->setValue($entity, $id);
 905
 906                $this->entityIdentifiers[$oid] = array($idField => $id);
 907                $this->entityStates[$oid] = self::STATE_MANAGED;
 908                $this->originalEntityData[$oid][$idField] = $id;
 909
 910                $this->addToIdentityMap($entity);
 911            }
 912        }
 913
 914        foreach ($entities as $entity) {
 915            if ($hasLifecycleCallbacks) {
 916                $class->invokeLifecycleCallbacks(Events::postPersist, $entity);
 917            }
 918
 919            if ($hasListeners) {
 920                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entity, $this->em));
 921            }
 922        }
 923    }
 924
 925    /**
 926     * Executes all entity updates for entities of the specified type.
 927     *
 928     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
 929     */
 930    private function executeUpdates($class)
 931    {
 932        $className = $class->name;
 933        $persister = $this->getEntityPersister($className);
 934
 935        $hasPreUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::preUpdate]);
 936        $hasPreUpdateListeners          = $this->evm->hasListeners(Events::preUpdate);
 937
 938        $hasPostUpdateLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postUpdate]);
 939        $hasPostUpdateListeners          = $this->evm->hasListeners(Events::postUpdate);
 940
 941        foreach ($this->entityUpdates as $oid => $entity) {
 942            if ( ! (get_class($entity) === $className || $entity instanceof Proxy && get_parent_class($entity) === $className)) {
 943                continue;
 944            }
 945
 946            if ($hasPreUpdateLifecycleCallbacks) {
 947                $class->invokeLifecycleCallbacks(Events::preUpdate, $entity);
 948
 949                $this->recomputeSingleEntityChangeSet($class, $entity);
 950            }
 951
 952            if ($hasPreUpdateListeners) {
 953                $this->evm->dispatchEvent(
 954                    Events::preUpdate,
 955                    new Event\PreUpdateEventArgs($entity, $this->em, $this->entityChangeSets[$oid])
 956                );
 957            }
 958
 959            if ($this->entityChangeSets[$oid]) {
 960                $persister->update($entity);
 961            }
 962
 963            unset($this->entityUpdates[$oid]);
 964
 965            if ($hasPostUpdateLifecycleCallbacks) {
 966                $class->invokeLifecycleCallbacks(Events::postUpdate, $entity);
 967            }
 968
 969            if ($hasPostUpdateListeners) {
 970                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entity, $this->em));
 971            }
 972        }
 973    }
 974
 975    /**
 976     * Executes all entity deletions for entities of the specified type.
 977     *
 978     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
 979     */
 980    private function executeDeletions($class)
 981    {
 982        $className = $class->name;
 983        $persister = $this->getEntityPersister($className);
 984
 985        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postRemove]);
 986        $hasListeners = $this->evm->hasListeners(Events::postRemove);
 987
 988        foreach ($this->entityDeletions as $oid => $entity) {
 989            if ( ! (get_class($entity) == $className || $entity instanceof Proxy && get_parent_class($entity) == $className)) {
 990                continue;
 991            }
 992
 993            $persister->delete($entity);
 994
 995            unset(
 996                $this->entityDeletions[$oid],
 997                $this->entityIdentifiers[$oid],
 998                $this->originalEntityData[$oid],
 999                $this->entityStates[$oid]
1000            );
1001
1002            // Entity with this $oid after deletion treated as NEW, even if the $oid
1003            // is obtained by a new entity because the old one went out of scope.
1004            //$this->entityStates[$oid] = self::STATE_NEW;
1005            if ( ! $class->isIdentifierNatural()) {
1006                $class->reflFields[$class->identifier[0]]->setValue($entity, null);
1007            }
1008
1009            if ($hasLifecycleCallbacks) {
1010                $class->invokeLifecycleCallbacks(Events::postRemove, $entity);
1011            }
1012
1013            if ($hasListeners) {
1014                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($entity, $this->em));
1015            }
1016        }
1017    }
1018
1019    /**
1020     * Gets the commit order.
1021     *
1022     * @return array
1023     */
1024    private function getCommitOrder(array $entityChangeSet = null)
1025    {
1026        if ($entityChangeSet === null) {
1027            $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions);
1028        }
1029
1030        $calc = $this->getCommitOrderCalculator();
1031
1032        // See if there are any new classes in the changeset, that are not in the
1033        // commit order graph yet (dont have a node).
1034        // We have to inspect changeSet to be able to correctly build dependencies.
1035        // It is not possible to use IdentityMap here because post inserted ids
1036        // are not yet available.
1037        $newNodes = array();
1038
1039        foreach ($entityChangeSet as $oid => $entity) {
1040            $className = get_class($entity);
1041
1042            if ($calc->hasClass($className)) {
1043                continue;
1044            }
1045
1046            $class = $this->em->getClassMetadata($className);
1047            $calc->addClass($class);
1048
1049            $newNodes[] = $class;
1050        }
1051
1052        // Calculate dependencies for new nodes
1053        while ($class = array_pop($newNodes)) {
1054            foreach ($class->associationMappings as $assoc) {
1055                if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
1056                    continue;
1057                }
1058
1059                $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1060
1061                if ( ! $calc->hasClass($targetClass->name)) {
1062                    $calc->addClass($targetClass);
1063
1064                    $newNodes[] = $targetClass;
1065                }
1066
1067                $calc->addDependency($targetClass, $class);
1068
1069                // If the target class has mapped subclasses, these share the same dependency.
1070                if ( ! $targetClass->subClasses) {
1071                    continue;
1072                }
1073
1074                foreach ($targetClass->subClasses as $subClassName) {
1075                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1076
1077                    if ( ! $calc->hasClass($subClassName)) {
1078                        $calc->addClass($targetSubClass);
1079
1080                        $newNodes[] = $targetSubClass;
1081                    }
1082
1083                    $calc->addDependency($targetSubClass, $class);
1084                }
1085            }
1086        }
1087
1088        return $calc->getCommitOrder();
1089    }
1090
1091    /**
1092     * Schedules an entity for insertion into the database.
1093     * If the entity already has an identifier, it will be added to the identity map.
1094     *
1095     * @param object $entity The entity to schedule for insertion.
1096     */
1097    public function scheduleForInsert($entity)
1098    {
1099        $oid = spl_object_hash($entity);
1100
1101        if (isset($this->entityUpdates[$oid])) {
1102            throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion.");
1103        }
1104
1105        if (isset($this->entityDeletions[$oid])) {
1106            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1107        }
1108        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1109            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1110        }
1111
1112        if (isset($this->entityInsertions[$oid])) {
1113            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1114        }
1115
1116        $this->entityInsertions[$oid] = $entity;
1117
1118        if (isset($this->entityIdentifiers[$oid])) {
1119            $this->addToIdentityMap($entity);
1120        }
1121    }
1122
1123    /**
1124     * Checks whether an entity is scheduled for insertion.
1125     *
1126     * @param object $entity
1127     * @return boolean
1128     */
1129    public function isScheduledForInsert($entity)
1130    {
1131        return isset($this->entityInsertions[spl_object_hash($entity)]);
1132    }
1133
1134    /**
1135     * Schedules an entity for being updated.
1136     *
1137     * @param object $entity The entity to schedule for being updated.
1138     */
1139    public function scheduleForUpdate($entity)
1140    {
1141        $oid = spl_object_hash($entity);
1142
1143        if ( ! isset($this->entityIdentifiers[$oid])) {
1144            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update");
1145        }
1146
1147        if (isset($this->entityDeletions[$oid])) {
1148            throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update");
1149        }
1150
1151        if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1152            $this->entityUpdates[$oid] = $entity;
1153        }
1154    }
1155
1156    /**
1157     * INTERNAL:
1158     * Schedules an extra update that will be executed immediately after the
1159     * regular entity updates within the currently running commit cycle.
1160     *
1161     * Extra updates for entities are stored as (entity, changeset) tuples.
1162     *
1163     * @ignore
1164     * @param object $entity The entity for which to schedule an extra update.
1165     * @param array $changeset The changeset of the entity (what to update).
1166     */
1167    public function scheduleExtraUpdate($entity, array $changeset)
1168    {
1169        $oid         = spl_object_hash($entity);
1170        $extraUpdate = array($entity, $changeset);
1171
1172        if (isset($this->extraUpdates[$oid])) {
1173            list($ignored, $changeset2) = $this->extraUpdates[$oid];
1174
1175            $extraUpdate = array($entity, $changeset + $changeset2);
1176        }
1177
1178        $this->extraUpdates[$oid] = $extraUpdate;
1179    }
1180
1181    /**
1182     * Checks whether an entity is registered as dirty in the unit of work.
1183     * Note: Is not very useful currently as dirty entities are only registered
1184     * at commit time.
1185     *
1186     * @param object $entity
1187     * @return boolean
1188     */
1189    public function isScheduledForUpdate($entity)
1190    {
1191        return isset($this->entityUpdates[spl_object_hash($entity)]);
1192    }
1193
1194
1195    /**
1196     * Checks whether an entity is registered to be checked in the unit of work.
1197     *
1198     * @param object $entity
1199     * @return boolean
1200     */
1201    public function isScheduledForDirtyCheck($entity)
1202    {
1203        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
1204
1205        return isset($this->scheduledForDirtyCheck[$rootEntityName][spl_object_hash($entity)]);
1206    }
1207
1208    /**
1209     * INTERNAL:
1210     * Schedules an entity for deletion.
1211     *
1212     * @param object $entity
1213     */
1214    public function scheduleForDelete($entity)
1215    {
1216        $oid = spl_object_hash($entity);
1217
1218        if (isset($this->entityInsertions[$oid])) {
1219            if ($this->isInIdentityMap($entity)) {
1220                $this->removeFromIdentityMap($entity);
1221            }
1222
1223            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1224
1225            return; // entity has not been persisted yet, so nothing more to do.
1226        }
1227
1228        if ( ! $this->isInIdentityMap($entity)) {
1229            return;
1230        }
1231
1232        $this->removeFromIdentityMap($entity);
1233
1234        if (isset($this->entityUpdates[$oid])) {
1235            unset($this->entityUpdates[$oid]);
1236        }
1237
1238        if ( ! isset($this->entityDeletions[$oid])) {
1239            $this->entityDeletions[$oid] = $entity;
1240            $this->entityStates[$oid]    = self::STATE_REMOVED;
1241        }
1242    }
1243
1244    /**
1245     * Checks whether an entity is registered as removed/deleted with the unit
1246     * of work.
1247     *
1248     * @param object $entity
1249     * @return boolean
1250     */
1251    public function isScheduledForDelete($entity)
1252    {
1253        return isset($this->entityDeletions[spl_object_hash($entity)]);
1254    }
1255
1256    /**
1257     * Checks whether an entity is scheduled for insertion, update or deletion.
1258     *
1259     * @param $entity
1260     * @return boolean
1261     */
1262    public function isEntityScheduled($entity)
1263    {
1264        $oid = spl_object_hash($entity);
1265
1266        return isset($this->entityInsertions[$oid])
1267            || isset($this->entityUpdates[$oid])
1268            || isset($this->entityDeletions[$oid]);
1269    }
1270
1271    /**
1272     * INTERNAL:
1273     * Registers an entity in the identity map.
1274     * Note that entities in a hierarchy are registered with the class name of
1275     * the root entity.
1276     *
1277     * @ignore
1278     * @param object $entity  The entity to register.
1279     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1280     *                  the entity in question is already managed.
1281     */
1282    public function addToIdentityMap($entity)
1283    {
1284        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1285        $idHash        = implode(' ', $this->entityIdentifiers[spl_object_hash($entity)]);
1286
1287        if ($idHash === '') {
1288            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
1289        }
1290
1291        $className = $classMetadata->rootEntityName;
1292
1293        if (isset($this->identityMap[$className][$idHash])) {
1294            return false;
1295        }
1296
1297        $this->identityMap[$className][$idHash] = $entity;
1298
1299        if ($entity instanceof NotifyPropertyChanged) {
1300            $entity->addPropertyChangedListener($this);
1301        }
1302
1303        return true;
1304    }
1305
1306    /**
1307     * Gets the state of an entity with regard to the current unit of work.
1308     *
1309     * @param object $entity
1310     * @param integer $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1311     *                        This parameter can be set to improve performance of entity state detection
1312     *                        by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1313     *                        is either known or does not matter for the caller of the method.
1314     * @return int The entity state.
1315     */
1316    public function getEntityState($entity, $assume = null)
1317    {
1318        $oid = spl_object_hash($entity);
1319
1320        if (isset($this->entityStates[$oid])) {
1321            return $this->entityStates[$oid];
1322        }
1323
1324        if ($assume !== null) {
1325            return $assume;
1326        }
1327
1328        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1329        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1330        // the UoW does not hold references to such objects and the object hash can be reused.
1331        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1332        $class = $this->em->getClassMetadata(get_class($entity));
1333        $id    = $class->getIdentifierValues($entity);
1334
1335        if ( ! $id) {
1336            return self::STATE_NEW;
1337        }
1338
1339        switch (true) {
1340            case ($class->isIdentifierNatural());
1341                // Check for a version field, if available, to avoid a db lookup.
1342                if ($class->isVersioned) {
1343                    return ($class->getFieldValue($entity, $class->versionField))
1344                        ? self::STATE_DETACHED
1345                        : self::STATE_NEW;
1346                }
1347
1348                // Last try before db lookup: check the identity map.
1349                if ($this->tryGetById($id, $class->rootEntityName)) {
1350                    return self::STATE_DETACHED;
1351                }
1352
1353                // db lookup
1354                if ($this->getEntityPersister(get_class($entity))->exists($entity)) {
1355                    return self::STATE_DETACHED;
1356                }
1357
1358                return self::STATE_NEW;
1359
1360            case ( ! $class->idGenerator->isPostInsertGenerator()):
1361                // if we have a pre insert generator we can't be sure that having an id
1362                // really means that the entity exists. We have to verify this through
1363                // the last resort: a db lookup
1364
1365                // Last try before db lookup: check the identity map.
1366                if ($this->tryGetById($id, $class->rootEntityName)) {
1367                    return self::STATE_DETACHED;
1368                }
1369
1370                // db lookup
1371                if ($this->getEntityPersister(get_class($entity))->exists($entity)) {
1372                    return self::STATE_DETACHED;
1373                }
1374
1375                return self::STATE_NEW;
1376
1377            default:
1378                return self::STATE_DETACHED;
1379        }
1380    }
1381
1382    /**
1383     * INTERNAL:
1384     * Removes an entity from the identity map. This effectively detaches the
1385     * entity from the persistence management of Doctrine.
1386     *
1387     * @ignore
1388     * @param object $entity
1389     * @return boolean
1390     */
1391    public function removeFromIdentityMap($entity)
1392    {
1393        $oid           = spl_object_hash($entity);
1394        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1395        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1396
1397        if ($idHash === '') {
1398            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "remove from identity map");
1399        }
1400
1401        $className = $classMetadata->rootEntityName;
1402
1403        if (isset($this->identityMap[$className][$idHash])) {
1404            unset($this->identityMap[$className][$idHash]);
1405            unset($this->readOnlyObjects[$oid]);
1406
1407            //$this->entityStates[$oid] = self::STATE_DETACHED;
1408
1409            return true;
1410        }
1411
1412        return false;
1413    }
1414
1415    /**
1416     * INTERNAL:
1417     * Gets an entity in the identity map by its identifier hash.
1418     *
1419     * @ignore
1420     * @param string $idHash
1421     * @param string $rootClassName
1422     * @return object
1423     */
1424    public function getByIdHash($idHash, $rootClassName)
1425    {
1426        return $this->identityMap[$rootClassName][$idHash];
1427    }
1428
1429    /**
1430     * INTERNAL:
1431     * Tries to get an entity by its identifier hash. If no entity is found for
1432     * the given hash, FALSE is returned.
1433     *
1434     * @ignore
1435     * @param string $idHash
1436     * @param string $rootClassName
1437     * @return mixed The found entity or FALSE.
1438     */
1439    public function tryGetByIdHash($idHash, $rootClassName)
1440    {
1441        if (isset($this->identityMap[$rootClassName][$idHash])) {
1442            return $this->identityMap[$rootClassName][$idHash];
1443        }
1444
1445        return false;
1446    }
1447
1448    /**
1449     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1450     *
1451     * @param object $entity
1452     * @return boolean
1453     */
1454    public function isInIdentityMap($e…

Large files files are truncated, but you can click here to view the full file