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

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