vendor/doctrine/orm/src/UnitOfWork.php line 2024

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