vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 2660

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