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

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