vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/UnitOfWork.php line 2869

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ODM\MongoDB;
  4. use Doctrine\Common\Collections\ArrayCollection;
  5. use Doctrine\Common\Collections\Collection;
  6. use Doctrine\Common\EventManager;
  7. use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
  8. use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
  9. use Doctrine\ODM\MongoDB\Mapping\MappingException;
  10. use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
  11. use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
  12. use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
  13. use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
  14. use Doctrine\ODM\MongoDB\Query\Query;
  15. use Doctrine\ODM\MongoDB\Types\DateType;
  16. use Doctrine\ODM\MongoDB\Types\Type;
  17. use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
  18. use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
  19. use Doctrine\Persistence\Mapping\ReflectionService;
  20. use Doctrine\Persistence\Mapping\RuntimeReflectionService;
  21. use Doctrine\Persistence\NotifyPropertyChanged;
  22. use Doctrine\Persistence\PropertyChangedListener;
  23. use InvalidArgumentException;
  24. use MongoDB\BSON\UTCDateTime;
  25. use MongoDB\Driver\WriteConcern;
  26. use ProxyManager\Proxy\GhostObjectInterface;
  27. use ReflectionProperty;
  28. use UnexpectedValueException;
  29. use function array_filter;
  30. use function assert;
  31. use function count;
  32. use function get_class;
  33. use function in_array;
  34. use function is_array;
  35. use function is_object;
  36. use function method_exists;
  37. use function preg_match;
  38. use function serialize;
  39. use function spl_object_hash;
  40. use function sprintf;
  41. /**
  42.  * The UnitOfWork is responsible for tracking changes to objects during an
  43.  * "object-level" transaction and for writing out changes to the database
  44.  * in the correct order.
  45.  *
  46.  * @psalm-import-type FieldMapping from ClassMetadata
  47.  * @psalm-import-type AssociationFieldMapping from ClassMetadata
  48.  * @psalm-type ChangeSet = array{
  49.  *      0: mixed,
  50.  *      1: mixed
  51.  * }
  52.  * @psalm-type Hints = array<int, mixed>
  53.  * @psalm-type CommitOptions array{
  54.  *      fsync?: bool,
  55.  *      safe?: int,
  56.  *      w?: int,
  57.  *      writeConcern?: WriteConcern
  58.  * }
  59.  */
  60. final class UnitOfWork implements PropertyChangedListener
  61. {
  62.     /**
  63.      * A document is in MANAGED state when its persistence is managed by a DocumentManager.
  64.      */
  65.     public const STATE_MANAGED 1;
  66.     /**
  67.      * A document is new if it has just been instantiated (i.e. using the "new" operator)
  68.      * and is not (yet) managed by a DocumentManager.
  69.      */
  70.     public const STATE_NEW 2;
  71.     /**
  72.      * A detached document is an instance with a persistent identity that is not
  73.      * (or no longer) associated with a DocumentManager (and a UnitOfWork).
  74.      */
  75.     public const STATE_DETACHED 3;
  76.     /**
  77.      * A removed document instance is an instance with a persistent identity,
  78.      * associated with a DocumentManager, whose persistent state has been
  79.      * deleted (or is scheduled for deletion).
  80.      */
  81.     public const STATE_REMOVED 4;
  82.     /**
  83.      * The identity map holds references to all managed documents.
  84.      *
  85.      * Documents are grouped by their class name, and then indexed by the
  86.      * serialized string of their database identifier field or, if the class
  87.      * has no identifier, the SPL object hash. Serializing the identifier allows
  88.      * differentiation of values that may be equal (via type juggling) but not
  89.      * identical.
  90.      *
  91.      * Since all classes in a hierarchy must share the same identifier set,
  92.      * we always take the root class name of the hierarchy.
  93.      *
  94.      * @var array
  95.      * @psalm-var array<class-string, array<string, object>>
  96.      */
  97.     private $identityMap = [];
  98.     /**
  99.      * Map of all identifiers of managed documents.
  100.      * Keys are object ids (spl_object_hash).
  101.      *
  102.      * @var array<string, mixed>
  103.      */
  104.     private $documentIdentifiers = [];
  105.     /**
  106.      * Map of the original document data of managed documents.
  107.      * Keys are object ids (spl_object_hash). This is used for calculating changesets
  108.      * at commit time.
  109.      *
  110.      * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
  111.      *           A value will only really be copied if the value in the document is modified
  112.      *           by the user.
  113.      *
  114.      * @var array<string, array<string, mixed>>
  115.      */
  116.     private $originalDocumentData = [];
  117.     /**
  118.      * Map of document changes. Keys are object ids (spl_object_hash).
  119.      * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
  120.      *
  121.      * @var array
  122.      * @psalm-var array<string, array<string, ChangeSet>>
  123.      */
  124.     private $documentChangeSets = [];
  125.     /**
  126.      * The (cached) states of any known documents.
  127.      * Keys are object ids (spl_object_hash).
  128.      *
  129.      * @var array
  130.      * @psalm-var array<string, self::STATE_*>
  131.      */
  132.     private $documentStates = [];
  133.     /**
  134.      * Map of documents that are scheduled for dirty checking at commit time.
  135.      *
  136.      * Documents are grouped by their class name, and then indexed by their SPL
  137.      * object hash. This is only used for documents with a change tracking
  138.      * policy of DEFERRED_EXPLICIT.
  139.      *
  140.      * @var array
  141.      * @psalm-var array<class-string, array<string, object>>
  142.      */
  143.     private $scheduledForSynchronization = [];
  144.     /**
  145.      * A list of all pending document insertions.
  146.      *
  147.      * @var array<string, object>
  148.      */
  149.     private $documentInsertions = [];
  150.     /**
  151.      * A list of all pending document updates.
  152.      *
  153.      * @var array<string, object>
  154.      */
  155.     private $documentUpdates = [];
  156.     /**
  157.      * A list of all pending document upserts.
  158.      *
  159.      * @var array<string, object>
  160.      */
  161.     private $documentUpserts = [];
  162.     /**
  163.      * A list of all pending document deletions.
  164.      *
  165.      * @var array<string, object>
  166.      */
  167.     private $documentDeletions = [];
  168.     /**
  169.      * All pending collection deletions.
  170.      *
  171.      * @var array
  172.      * @psalm-var array<string, PersistentCollectionInterface<array-key, object>>
  173.      */
  174.     private $collectionDeletions = [];
  175.     /**
  176.      * All pending collection updates.
  177.      *
  178.      * @var array
  179.      * @psalm-var array<string, PersistentCollectionInterface<array-key, object>>
  180.      */
  181.     private $collectionUpdates = [];
  182.     /**
  183.      * A list of documents related to collections scheduled for update or deletion
  184.      *
  185.      * @var array
  186.      * @psalm-var array<string, array<string, PersistentCollectionInterface<array-key, object>>>
  187.      */
  188.     private $hasScheduledCollections = [];
  189.     /**
  190.      * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
  191.      * At the end of the UnitOfWork all these collections will make new snapshots
  192.      * of their data.
  193.      *
  194.      * @psalm-var array<string, array<PersistentCollectionInterface<array-key, object>>>
  195.      */
  196.     private $visitedCollections = [];
  197.     /**
  198.      * The DocumentManager that "owns" this UnitOfWork instance.
  199.      *
  200.      * @var DocumentManager
  201.      */
  202.     private $dm;
  203.     /**
  204.      * The EventManager used for dispatching events.
  205.      *
  206.      * @var EventManager
  207.      */
  208.     private $evm;
  209.     /**
  210.      * Additional documents that are scheduled for removal.
  211.      *
  212.      * @var array<string, object>
  213.      */
  214.     private $orphanRemovals = [];
  215.     /**
  216.      * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
  217.      *
  218.      * @var HydratorFactory
  219.      */
  220.     private $hydratorFactory;
  221.     /**
  222.      * The document persister instances used to persist document instances.
  223.      *
  224.      * @var array
  225.      * @psalm-var array<class-string, Persisters\DocumentPersister>
  226.      */
  227.     private $persisters = [];
  228.     /**
  229.      * The collection persister instance used to persist changes to collections.
  230.      *
  231.      * @var Persisters\CollectionPersister|null
  232.      */
  233.     private $collectionPersister;
  234.     /**
  235.      * The persistence builder instance used in DocumentPersisters.
  236.      *
  237.      * @var PersistenceBuilder|null
  238.      */
  239.     private $persistenceBuilder;
  240.     /**
  241.      * Array of parent associations between embedded documents.
  242.      *
  243.      * @var array
  244.      * @psalm-var array<string, array{0: AssociationFieldMapping, 1: object|null, 2: string}>
  245.      */
  246.     private $parentAssociations = [];
  247.     /** @var LifecycleEventManager */
  248.     private $lifecycleEventManager;
  249.     /** @var ReflectionService */
  250.     private $reflectionService;
  251.     /**
  252.      * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
  253.      * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
  254.      * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
  255.      *
  256.      * @var array<string, object>
  257.      */
  258.     private $embeddedDocumentsRegistry = [];
  259.     /** @var int */
  260.     private $commitsInProgress 0;
  261.     /**
  262.      * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
  263.      */
  264.     public function __construct(DocumentManager $dmEventManager $evmHydratorFactory $hydratorFactory)
  265.     {
  266.         $this->dm                    $dm;
  267.         $this->evm                   $evm;
  268.         $this->hydratorFactory       $hydratorFactory;
  269.         $this->lifecycleEventManager = new LifecycleEventManager($dm$this$evm);
  270.         $this->reflectionService     = new RuntimeReflectionService();
  271.     }
  272.     /**
  273.      * Factory for returning new PersistenceBuilder instances used for preparing data into
  274.      * queries for insert persistence.
  275.      *
  276.      * @internal
  277.      */
  278.     public function getPersistenceBuilder(): PersistenceBuilder
  279.     {
  280.         if (! $this->persistenceBuilder) {
  281.             $this->persistenceBuilder = new PersistenceBuilder($this->dm$this);
  282.         }
  283.         return $this->persistenceBuilder;
  284.     }
  285.     /**
  286.      * Sets the parent association for a given embedded document.
  287.      *
  288.      * @internal
  289.      *
  290.      * @psalm-param FieldMapping $mapping
  291.      */
  292.     public function setParentAssociation(object $document, array $mapping, ?object $parentstring $propertyPath): void
  293.     {
  294.         $oid                                   spl_object_hash($document);
  295.         $this->embeddedDocumentsRegistry[$oid] = $document;
  296.         $this->parentAssociations[$oid]        = [$mapping$parent$propertyPath];
  297.     }
  298.     /**
  299.      * Gets the parent association for a given embedded document.
  300.      *
  301.      *     <code>
  302.      *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
  303.      *     </code>
  304.      *
  305.      * @psalm-return array{0: AssociationFieldMapping, 1: object|null, 2: string}|null
  306.      */
  307.     public function getParentAssociation(object $document): ?array
  308.     {
  309.         $oid spl_object_hash($document);
  310.         return $this->parentAssociations[$oid] ?? null;
  311.     }
  312.     /**
  313.      * Get the document persister instance for the given document name
  314.      *
  315.      * @psalm-param class-string<T> $documentName
  316.      *
  317.      * @psalm-return Persisters\DocumentPersister<T>
  318.      *
  319.      * @template T of object
  320.      */
  321.     public function getDocumentPersister(string $documentName): Persisters\DocumentPersister
  322.     {
  323.         if (! isset($this->persisters[$documentName])) {
  324.             $class                           $this->dm->getClassMetadata($documentName);
  325.             $pb                              $this->getPersistenceBuilder();
  326.             $this->persisters[$documentName] = new Persisters\DocumentPersister($pb$this->dm$this$this->hydratorFactory$class);
  327.         }
  328.         /** @psalm-var Persisters\DocumentPersister<T> */
  329.         return $this->persisters[$documentName];
  330.     }
  331.     /**
  332.      * Get the collection persister instance.
  333.      */
  334.     public function getCollectionPersister(): CollectionPersister
  335.     {
  336.         if (! isset($this->collectionPersister)) {
  337.             $pb                        $this->getPersistenceBuilder();
  338.             $this->collectionPersister = new Persisters\CollectionPersister($this->dm$pb$this);
  339.         }
  340.         return $this->collectionPersister;
  341.     }
  342.     /**
  343.      * Set the document persister instance to use for the given document name
  344.      *
  345.      * @internal
  346.      *
  347.      * @psalm-param class-string<T> $documentName
  348.      * @psalm-param Persisters\DocumentPersister<T> $persister
  349.      *
  350.      * @template T of object
  351.      */
  352.     public function setDocumentPersister(string $documentNamePersisters\DocumentPersister $persister): void
  353.     {
  354.         $this->persisters[$documentName] = $persister;
  355.     }
  356.     /**
  357.      * Commits the UnitOfWork, executing all operations that have been postponed
  358.      * up to this point. The state of all managed documents will be synchronized with
  359.      * the database.
  360.      *
  361.      * The operations are executed in the following order:
  362.      *
  363.      * 1) All document insertions
  364.      * 2) All document updates
  365.      * 3) All document deletions
  366.      *
  367.      * @param array $options Array of options to be used with batchInsert(), update() and remove()
  368.      * @psalm-param CommitOptions $options
  369.      */
  370.     public function commit(array $options = []): void
  371.     {
  372.         // Raise preFlush
  373.         if ($this->evm->hasListeners(Events::preFlush)) {
  374.             $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
  375.         }
  376.         // Compute changes done since last commit.
  377.         $this->computeChangeSets();
  378.         if (
  379.             ! ($this->documentInsertions ||
  380.             $this->documentUpserts ||
  381.             $this->documentDeletions ||
  382.             $this->documentUpdates ||
  383.             $this->collectionUpdates ||
  384.             $this->collectionDeletions ||
  385.             $this->orphanRemovals)
  386.         ) {
  387.             return; // Nothing to do.
  388.         }
  389.         $this->commitsInProgress++;
  390.         if ($this->commitsInProgress 1) {
  391.             throw MongoDBException::commitInProgress();
  392.         }
  393.         try {
  394.             if ($this->orphanRemovals) {
  395.                 foreach ($this->orphanRemovals as $removal) {
  396.                     $this->remove($removal);
  397.                 }
  398.             }
  399.             // Raise onFlush
  400.             if ($this->evm->hasListeners(Events::onFlush)) {
  401.                 $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
  402.             }
  403.             foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
  404.                 [$class$documents] = $classAndDocuments;
  405.                 $this->executeUpserts($class$documents$options);
  406.             }
  407.             foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
  408.                 [$class$documents] = $classAndDocuments;
  409.                 $this->executeInserts($class$documents$options);
  410.             }
  411.             foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
  412.                 [$class$documents] = $classAndDocuments;
  413.                 $this->executeUpdates($class$documents$options);
  414.             }
  415.             foreach ($this->getClassesForCommitAction($this->documentDeletionstrue) as $classAndDocuments) {
  416.                 [$class$documents] = $classAndDocuments;
  417.                 $this->executeDeletions($class$documents$options);
  418.             }
  419.             // Raise postFlush
  420.             if ($this->evm->hasListeners(Events::postFlush)) {
  421.                 $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
  422.             }
  423.             // Clear up
  424.             $this->documentInsertions          =
  425.             $this->documentUpserts             =
  426.             $this->documentUpdates             =
  427.             $this->documentDeletions           =
  428.             $this->documentChangeSets          =
  429.             $this->collectionUpdates           =
  430.             $this->collectionDeletions         =
  431.             $this->visitedCollections          =
  432.             $this->scheduledForSynchronization =
  433.             $this->orphanRemovals              =
  434.             $this->hasScheduledCollections     = [];
  435.         } finally {
  436.             $this->commitsInProgress--;
  437.         }
  438.     }
  439.     /**
  440.      * Groups a list of scheduled documents by their class.
  441.      *
  442.      * @param array<string, object> $documents
  443.      *
  444.      * @psalm-return array<class-string, array{0: ClassMetadata<object>, 1: array<string, object>}>
  445.      */
  446.     private function getClassesForCommitAction(array $documentsbool $includeEmbedded false): array
  447.     {
  448.         if (empty($documents)) {
  449.             return [];
  450.         }
  451.         $divided = [];
  452.         $embeds  = [];
  453.         foreach ($documents as $oid => $d) {
  454.             $className get_class($d);
  455.             if (isset($embeds[$className])) {
  456.                 continue;
  457.             }
  458.             if (isset($divided[$className])) {
  459.                 $divided[$className][1][$oid] = $d;
  460.                 continue;
  461.             }
  462.             $class $this->dm->getClassMetadata($className);
  463.             if ($class->isEmbeddedDocument && ! $includeEmbedded) {
  464.                 $embeds[$className] = true;
  465.                 continue;
  466.             }
  467.             if ($class->isView()) {
  468.                 continue;
  469.             }
  470.             if (empty($divided[$class->name])) {
  471.                 $divided[$class->name] = [$class, [$oid => $d]];
  472.             } else {
  473.                 $divided[$class->name][1][$oid] = $d;
  474.             }
  475.         }
  476.         return $divided;
  477.     }
  478.     /**
  479.      * Compute changesets of all documents scheduled for insertion.
  480.      *
  481.      * Embedded documents will not be processed.
  482.      */
  483.     private function computeScheduleInsertsChangeSets(): void
  484.     {
  485.         foreach ($this->documentInsertions as $document) {
  486.             $class $this->dm->getClassMetadata(get_class($document));
  487.             if ($class->isEmbeddedDocument || $class->isView()) {
  488.                 continue;
  489.             }
  490.             $this->computeChangeSet($class$document);
  491.         }
  492.     }
  493.     /**
  494.      * Compute changesets of all documents scheduled for upsert.
  495.      *
  496.      * Embedded documents will not be processed.
  497.      */
  498.     private function computeScheduleUpsertsChangeSets(): void
  499.     {
  500.         foreach ($this->documentUpserts as $document) {
  501.             $class $this->dm->getClassMetadata(get_class($document));
  502.             if ($class->isEmbeddedDocument || $class->isView()) {
  503.                 continue;
  504.             }
  505.             $this->computeChangeSet($class$document);
  506.         }
  507.     }
  508.     /**
  509.      * Gets the changeset for a document.
  510.      *
  511.      * @return array array('property' => array(0 => mixed, 1 => mixed))
  512.      * @psalm-return array<string, ChangeSet>
  513.      */
  514.     public function getDocumentChangeSet(object $document): array
  515.     {
  516.         $oid spl_object_hash($document);
  517.         return $this->documentChangeSets[$oid] ?? [];
  518.     }
  519.     /**
  520.      * Sets the changeset for a document.
  521.      *
  522.      * @internal
  523.      *
  524.      * @psalm-param array<string, ChangeSet> $changeset
  525.      */
  526.     public function setDocumentChangeSet(object $document, array $changeset): void
  527.     {
  528.         $this->documentChangeSets[spl_object_hash($document)] = $changeset;
  529.     }
  530.     /**
  531.      * Get a documents actual data, flattening all the objects to arrays.
  532.      *
  533.      * @internal
  534.      *
  535.      * @return array<string, mixed>
  536.      */
  537.     public function getDocumentActualData(object $document): array
  538.     {
  539.         $class      $this->dm->getClassMetadata(get_class($document));
  540.         $actualData = [];
  541.         foreach ($class->reflFields as $name => $refProp) {
  542.             $mapping $class->fieldMappings[$name];
  543.             // skip not saved fields
  544.             if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
  545.                 continue;
  546.             }
  547.             $value $refProp->getValue($document);
  548.             if (
  549.                 (isset($mapping['association']) && $mapping['type'] === ClassMetadata::MANY)
  550.                 && $value !== null && ! ($value instanceof PersistentCollectionInterface)
  551.             ) {
  552.                 // If $actualData[$name] is not a Collection then use an ArrayCollection.
  553.                 if (! $value instanceof Collection) {
  554.                     $value = new ArrayCollection($value);
  555.                 }
  556.                 // Inject PersistentCollection
  557.                 $coll $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm$mapping$value);
  558.                 $coll->setOwner($document$mapping);
  559.                 $coll->setDirty(! $value->isEmpty());
  560.                 $class->reflFields[$name]->setValue($document$coll);
  561.                 $actualData[$name] = $coll;
  562.             } else {
  563.                 $actualData[$name] = $value;
  564.             }
  565.         }
  566.         return $actualData;
  567.     }
  568.     /**
  569.      * Computes the changes that happened to a single document.
  570.      *
  571.      * Modifies/populates the following properties:
  572.      *
  573.      * {@link originalDocumentData}
  574.      * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
  575.      * then it was not fetched from the database and therefore we have no original
  576.      * document data yet. All of the current document data is stored as the original document data.
  577.      *
  578.      * {@link documentChangeSets}
  579.      * The changes detected on all properties of the document are stored there.
  580.      * A change is a tuple array where the first entry is the old value and the second
  581.      * entry is the new value of the property. Changesets are used by persisters
  582.      * to INSERT/UPDATE the persistent document state.
  583.      *
  584.      * {@link documentUpdates}
  585.      * If the document is already fully MANAGED (has been fetched from the database before)
  586.      * and any changes to its properties are detected, then a reference to the document is stored
  587.      * there to mark it for an update.
  588.      *
  589.      * @psalm-param ClassMetadata<T> $class
  590.      * @psalm-param T $document
  591.      *
  592.      * @template T of object
  593.      */
  594.     public function computeChangeSet(ClassMetadata $classobject $document): void
  595.     {
  596.         if (! $class->isInheritanceTypeNone()) {
  597.             $class $this->dm->getClassMetadata(get_class($document));
  598.         }
  599.         // Fire PreFlush lifecycle callbacks
  600.         if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
  601.             $class->invokeLifecycleCallbacks(Events::preFlush$document, [new Event\PreFlushEventArgs($this->dm)]);
  602.         }
  603.         $this->computeOrRecomputeChangeSet($class$document);
  604.     }
  605.     /**
  606.      * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
  607.      *
  608.      * @psalm-param ClassMetadata<T> $class
  609.      * @psalm-param T $document
  610.      *
  611.      * @template T of object
  612.      */
  613.     private function computeOrRecomputeChangeSet(ClassMetadata $classobject $documentbool $recompute false): void
  614.     {
  615.         if ($class->isView()) {
  616.             return;
  617.         }
  618.         $oid           spl_object_hash($document);
  619.         $actualData    $this->getDocumentActualData($document);
  620.         $isNewDocument = ! isset($this->originalDocumentData[$oid]);
  621.         if ($isNewDocument) {
  622.             // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
  623.             // These result in an INSERT.
  624.             $this->originalDocumentData[$oid] = $actualData;
  625.             $changeSet                        = [];
  626.             foreach ($actualData as $propName => $actualValue) {
  627.                 /* At this PersistentCollection shouldn't be here, probably it
  628.                  * was cloned and its ownership must be fixed
  629.                  */
  630.                 if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
  631.                     $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue$document$class$propName);
  632.                     $actualValue           $actualData[$propName];
  633.                 }
  634.                 // ignore inverse side of reference relationship
  635.                 if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
  636.                     continue;
  637.                 }
  638.                 $changeSet[$propName] = [null$actualValue];
  639.             }
  640.             $this->documentChangeSets[$oid] = $changeSet;
  641.         } else {
  642.             if ($class->isReadOnly) {
  643.                 return;
  644.             }
  645.             // Document is "fully" MANAGED: it was already fully persisted before
  646.             // and we have a copy of the original data
  647.             $originalData           $this->originalDocumentData[$oid];
  648.             $isChangeTrackingNotify $class->isChangeTrackingNotify();
  649.             if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
  650.                 $changeSet $this->documentChangeSets[$oid];
  651.             } else {
  652.                 $changeSet = [];
  653.             }
  654.             $gridFSMetadataProperty null;
  655.             if ($class->isFile) {
  656.                 try {
  657.                     $gridFSMetadata         $class->getFieldMappingByDbFieldName('metadata');
  658.                     $gridFSMetadataProperty $gridFSMetadata['fieldName'];
  659.                 } catch (MappingException $e) {
  660.                 }
  661.             }
  662.             foreach ($actualData as $propName => $actualValue) {
  663.                 // skip not saved fields
  664.                 if (
  665.                     (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
  666.                     ($class->isFile && $propName !== $gridFSMetadataProperty)
  667.                 ) {
  668.                     continue;
  669.                 }
  670.                 $orgValue $originalData[$propName] ?? null;
  671.                 // skip if value has not changed
  672.                 if ($orgValue === $actualValue) {
  673.                     if (! $actualValue instanceof PersistentCollectionInterface) {
  674.                         continue;
  675.                     }
  676.                     if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
  677.                         // consider dirty collections as changed as well
  678.                         continue;
  679.                     }
  680.                 }
  681.                 // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
  682.                 if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === ClassMetadata::ONE) {
  683.                     if ($orgValue !== null) {
  684.                         $this->scheduleOrphanRemoval($orgValue);
  685.                     }
  686.                     $changeSet[$propName] = [$orgValue$actualValue];
  687.                     continue;
  688.                 }
  689.                 // if owning side of reference-one relationship
  690.                 if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === ClassMetadata::ONE && $class->fieldMappings[$propName]['isOwningSide']) {
  691.                     if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
  692.                         $this->scheduleOrphanRemoval($orgValue);
  693.                     }
  694.                     $changeSet[$propName] = [$orgValue$actualValue];
  695.                     continue;
  696.                 }
  697.                 if ($isChangeTrackingNotify) {
  698.                     continue;
  699.                 }
  700.                 // ignore inverse side of reference relationship
  701.                 if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
  702.                     continue;
  703.                 }
  704.                 // Persistent collection was exchanged with the "originally"
  705.                 // created one. This can only mean it was cloned and replaced
  706.                 // on another document.
  707.                 if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
  708.                     $actualValue $this->fixPersistentCollectionOwnership($actualValue$document$class$propName);
  709.                 }
  710.                 // if embed-many or reference-many relationship
  711.                 if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === ClassMetadata::MANY) {
  712.                     $changeSet[$propName] = [$orgValue$actualValue];
  713.                     /* If original collection was exchanged with a non-empty value
  714.                      * and $set will be issued, there is no need to $unset it first
  715.                      */
  716.                     if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
  717.                         continue;
  718.                     }
  719.                     if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
  720.                         $this->scheduleCollectionDeletion($orgValue);
  721.                     }
  722.                     continue;
  723.                 }
  724.                 // skip equivalent date values
  725.                 if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
  726.                     $dateType Type::getType('date');
  727.                     assert($dateType instanceof DateType);
  728.                     $dbOrgValue    $dateType->convertToDatabaseValue($orgValue);
  729.                     $dbActualValue $dateType->convertToDatabaseValue($actualValue);
  730.                     $orgTimestamp    $dbOrgValue instanceof UTCDateTime $dbOrgValue->toDateTime()->getTimestamp() : null;
  731.                     $actualTimestamp $dbActualValue instanceof UTCDateTime $dbActualValue->toDateTime()->getTimestamp() : null;
  732.                     if ($orgTimestamp === $actualTimestamp) {
  733.                         continue;
  734.                     }
  735.                 }
  736.                 // regular field
  737.                 $changeSet[$propName] = [$orgValue$actualValue];
  738.             }
  739.             if ($changeSet) {
  740.                 $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
  741.                     ? $changeSet $this->documentChangeSets[$oid]
  742.                     : $changeSet;
  743.                 $this->originalDocumentData[$oid] = $actualData;
  744.                 $this->scheduleForUpdate($document);
  745.             }
  746.         }
  747.         // Look for changes in associations of the document
  748.         $associationMappings array_filter(
  749.             $class->associationMappings,
  750.             static function ($assoc) {
  751.                 return empty($assoc['notSaved']);
  752.             }
  753.         );
  754.         foreach ($associationMappings as $mapping) {
  755.             $value $class->reflFields[$mapping['fieldName']]->getValue($document);
  756.             if ($value === null) {
  757.                 continue;
  758.             }
  759.             $this->computeAssociationChanges($document$mapping$value);
  760.             if (isset($mapping['reference'])) {
  761.                 continue;
  762.             }
  763.             $values $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
  764.             foreach ($values as $obj) {
  765.                 $oid2 spl_object_hash($obj);
  766.                 if (! isset($this->documentChangeSets[$oid2])) {
  767.                     continue;
  768.                 }
  769.                 if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
  770.                     // instance of $value is the same as it was previously otherwise there would be
  771.                     // change set already in place
  772.                     $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value$value];
  773.                 }
  774.                 if (! $isNewDocument) {
  775.                     $this->scheduleForUpdate($document);
  776.                 }
  777.                 break;
  778.             }
  779.         }
  780.     }
  781.     /**
  782.      * Computes all the changes that have been done to documents and collections
  783.      * since the last commit and stores these changes in the _documentChangeSet map
  784.      * temporarily for access by the persisters, until the UoW commit is finished.
  785.      */
  786.     public function computeChangeSets(): void
  787.     {
  788.         $this->computeScheduleInsertsChangeSets();
  789.         $this->computeScheduleUpsertsChangeSets();
  790.         // Compute changes for other MANAGED documents. Change tracking policies take effect here.
  791.         foreach ($this->identityMap as $className => $documents) {
  792.             $class $this->dm->getClassMetadata($className);
  793.             if ($class->isEmbeddedDocument || $class->isView()) {
  794.                 /* we do not want to compute changes to embedded documents up front
  795.                  * in case embedded document was replaced and its changeset
  796.                  * would corrupt data. Embedded documents' change set will
  797.                  * be calculated by reachability from owning document.
  798.                  */
  799.                 continue;
  800.             }
  801.             // If change tracking is explicit or happens through notification, then only compute
  802.             // changes on document of that type that are explicitly marked for synchronization.
  803.             switch (true) {
  804.                 case $class->isChangeTrackingDeferredImplicit():
  805.                     $documentsToProcess $documents;
  806.                     break;
  807.                 case isset($this->scheduledForSynchronization[$className]):
  808.                     $documentsToProcess $this->scheduledForSynchronization[$className];
  809.                     break;
  810.                 default:
  811.                     $documentsToProcess = [];
  812.             }
  813.             foreach ($documentsToProcess as $document) {
  814.                 // Ignore uninitialized proxy objects
  815.                 if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  816.                     continue;
  817.                 }
  818.                 // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
  819.                 $oid spl_object_hash($document);
  820.                 if (
  821.                     isset($this->documentInsertions[$oid])
  822.                     || isset($this->documentUpserts[$oid])
  823.                     || isset($this->documentDeletions[$oid])
  824.                     || ! isset($this->documentStates[$oid])
  825.                 ) {
  826.                     continue;
  827.                 }
  828.                 $this->computeChangeSet($class$document);
  829.             }
  830.         }
  831.     }
  832.     /**
  833.      * Computes the changes of an association.
  834.      *
  835.      * @param mixed $value The value of the association.
  836.      * @psalm-param AssociationFieldMapping $assoc
  837.      *
  838.      * @throws InvalidArgumentException
  839.      */
  840.     private function computeAssociationChanges(object $parentDocument, array $assoc$value): void
  841.     {
  842.         $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
  843.         $class                 $this->dm->getClassMetadata(get_class($parentDocument));
  844.         $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
  845.         if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
  846.             return;
  847.         }
  848.         if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
  849.             if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
  850.                 $this->scheduleCollectionUpdate($value);
  851.             }
  852.             $topmostOwner                                               $this->getOwningDocument($value->getOwner());
  853.             $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
  854.             if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
  855.                 $value->initialize();
  856.                 foreach ($value->getDeletedDocuments() as $orphan) {
  857.                     $this->scheduleOrphanRemoval($orphan);
  858.                 }
  859.             }
  860.         }
  861.         // Look through the documents, and in any of their associations,
  862.         // for transient (new) documents, recursively. ("Persistence by reachability")
  863.         // Unwrap. Uninitialized collections will simply be empty.
  864.         $unwrappedValue $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
  865.         $count 0;
  866.         foreach ($unwrappedValue as $key => $entry) {
  867.             if (! is_object($entry)) {
  868.                 throw new InvalidArgumentException(
  869.                     sprintf('Expected object, found "%s" in %s::%s'$entryget_class($parentDocument), $assoc['name'])
  870.                 );
  871.             }
  872.             $targetClass $this->dm->getClassMetadata(get_class($entry));
  873.             $state $this->getDocumentState($entryself::STATE_NEW);
  874.             // Handle "set" strategy for multi-level hierarchy
  875.             $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count $key;
  876.             $path    $assoc['type'] === ClassMetadata::MANY $assoc['name'] . '.' $pathKey $assoc['name'];
  877.             $count++;
  878.             switch ($state) {
  879.                 case self::STATE_NEW:
  880.                     if (! $assoc['isCascadePersist']) {
  881.                         throw new InvalidArgumentException('A new document was found through a relationship that was not'
  882.                             ' configured to cascade persist operations: ' $this->objToStr($entry) . '.'
  883.                             ' Explicitly persist the new document or configure cascading persist operations'
  884.                             ' on the relationship.');
  885.                     }
  886.                     $this->persistNew($targetClass$entry);
  887.                     $this->setParentAssociation($entry$assoc$parentDocument$path);
  888.                     $this->computeChangeSet($targetClass$entry);
  889.                     break;
  890.                 case self::STATE_MANAGED:
  891.                     if ($targetClass->isEmbeddedDocument) {
  892.                         [, $knownParent] = $this->getParentAssociation($entry);
  893.                         if ($knownParent && $knownParent !== $parentDocument) {
  894.                             $entry = clone $entry;
  895.                             if ($assoc['type'] === ClassMetadata::ONE) {
  896.                                 $class->setFieldValue($parentDocument$assoc['fieldName'], $entry);
  897.                                 $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
  898.                                 $poid spl_object_hash($parentDocument);
  899.                                 if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
  900.                                     $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
  901.                                 }
  902.                             } else {
  903.                                 // must use unwrapped value to not trigger orphan removal
  904.                                 $unwrappedValue[$key] = $entry;
  905.                             }
  906.                             $this->persistNew($targetClass$entry);
  907.                         }
  908.                         $this->setParentAssociation($entry$assoc$parentDocument$path);
  909.                         $this->computeChangeSet($targetClass$entry);
  910.                     }
  911.                     break;
  912.                 case self::STATE_REMOVED:
  913.                     // Consume the $value as array (it's either an array or an ArrayAccess)
  914.                     // and remove the element from Collection.
  915.                     if ($assoc['type'] === ClassMetadata::MANY) {
  916.                         unset($value[$key]);
  917.                     }
  918.                     break;
  919.                 case self::STATE_DETACHED:
  920.                     // Can actually not happen right now as we assume STATE_NEW,
  921.                     // so the exception will be raised from the DBAL layer (constraint violation).
  922.                     throw new InvalidArgumentException('A detached document was found through a '
  923.                         'relationship during cascading a persist operation.');
  924.                 default:
  925.                     // MANAGED associated documents are already taken into account
  926.                     // during changeset calculation anyway, since they are in the identity map.
  927.             }
  928.         }
  929.     }
  930.     /**
  931.      * Computes the changeset of an individual document, independently of the
  932.      * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
  933.      *
  934.      * The passed document must be a managed document. If the document already has a change set
  935.      * because this method is invoked during a commit cycle then the change sets are added.
  936.      * whereby changes detected in this method prevail.
  937.      *
  938.      * @psalm-param ClassMetadata<T> $class
  939.      * @psalm-param T $document
  940.      *
  941.      * @throws InvalidArgumentException If the passed document is not MANAGED.
  942.      *
  943.      * @template T of object
  944.      */
  945.     public function recomputeSingleDocumentChangeSet(ClassMetadata $classobject $document): void
  946.     {
  947.         // Ignore uninitialized proxy objects
  948.         if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  949.             return;
  950.         }
  951.         $oid spl_object_hash($document);
  952.         if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
  953.             throw new InvalidArgumentException('Document must be managed.');
  954.         }
  955.         if (! $class->isInheritanceTypeNone()) {
  956.             $class $this->dm->getClassMetadata(get_class($document));
  957.         }
  958.         $this->computeOrRecomputeChangeSet($class$documenttrue);
  959.     }
  960.     /**
  961.      * @psalm-param ClassMetadata<T> $class
  962.      * @psalm-param T $document
  963.      *
  964.      * @throws InvalidArgumentException If there is something wrong with document's identifier.
  965.      *
  966.      * @template T of object
  967.      */
  968.     private function persistNew(ClassMetadata $classobject $document): void
  969.     {
  970.         $this->lifecycleEventManager->prePersist($class$document);
  971.         $oid    spl_object_hash($document);
  972.         $upsert false;
  973.         if ($class->identifier) {
  974.             $idValue $class->getIdentifierValue($document);
  975.             $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
  976.             if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
  977.                 throw new InvalidArgumentException(sprintf(
  978.                     '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
  979.                     get_class($document)
  980.                 ));
  981.             }
  982.             if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
  983.                 throw new InvalidArgumentException(sprintf(
  984.                     '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
  985.                     get_class($document)
  986.                 ));
  987.             }
  988.             if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
  989.                 $idValue $class->idGenerator->generate($this->dm$document);
  990.                 $idValue $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
  991.                 $class->setIdentifierValue($document$idValue);
  992.             }
  993.             $this->documentIdentifiers[$oid] = $idValue;
  994.         } else {
  995.             // this is for embedded documents without identifiers
  996.             $this->documentIdentifiers[$oid] = $oid;
  997.         }
  998.         $this->documentStates[$oid] = self::STATE_MANAGED;
  999.         if ($upsert) {
  1000.             $this->scheduleForUpsert($class$document);
  1001.         } else {
  1002.             $this->scheduleForInsert($class$document);
  1003.         }
  1004.     }
  1005.     /**
  1006.      * Executes all document insertions for documents of the specified type.
  1007.      *
  1008.      * @psalm-param ClassMetadata<T> $class
  1009.      * @psalm-param T[] $documents
  1010.      * @psalm-param CommitOptions $options
  1011.      *
  1012.      * @template T of object
  1013.      */
  1014.     private function executeInserts(ClassMetadata $class, array $documents, array $options = []): void
  1015.     {
  1016.         $persister $this->getDocumentPersister($class->name);
  1017.         foreach ($documents as $oid => $document) {
  1018.             $persister->addInsert($document);
  1019.             unset($this->documentInsertions[$oid]);
  1020.         }
  1021.         $persister->executeInserts($options);
  1022.         foreach ($documents as $document) {
  1023.             $this->lifecycleEventManager->postPersist($class$document);
  1024.         }
  1025.     }
  1026.     /**
  1027.      * Executes all document upserts for documents of the specified type.
  1028.      *
  1029.      * @psalm-param ClassMetadata<T> $class
  1030.      * @psalm-param T[] $documents
  1031.      * @psalm-param CommitOptions $options
  1032.      *
  1033.      * @template T of object
  1034.      */
  1035.     private function executeUpserts(ClassMetadata $class, array $documents, array $options = []): void
  1036.     {
  1037.         $persister $this->getDocumentPersister($class->name);
  1038.         foreach ($documents as $oid => $document) {
  1039.             $persister->addUpsert($document);
  1040.             unset($this->documentUpserts[$oid]);
  1041.         }
  1042.         $persister->executeUpserts($options);
  1043.         foreach ($documents as $document) {
  1044.             $this->lifecycleEventManager->postPersist($class$document);
  1045.         }
  1046.     }
  1047.     /**
  1048.      * Executes all document updates for documents of the specified type.
  1049.      *
  1050.      * @psalm-param ClassMetadata<T> $class
  1051.      * @psalm-param T[] $documents
  1052.      * @psalm-param CommitOptions $options
  1053.      *
  1054.      * @template T of object
  1055.      */
  1056.     private function executeUpdates(ClassMetadata $class, array $documents, array $options = []): void
  1057.     {
  1058.         if ($class->isReadOnly) {
  1059.             return;
  1060.         }
  1061.         $className $class->name;
  1062.         $persister $this->getDocumentPersister($className);
  1063.         foreach ($documents as $oid => $document) {
  1064.             $this->lifecycleEventManager->preUpdate($class$document);
  1065.             if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
  1066.                 $persister->update($document$options);
  1067.             }
  1068.             unset($this->documentUpdates[$oid]);
  1069.             $this->lifecycleEventManager->postUpdate($class$document);
  1070.         }
  1071.     }
  1072.     /**
  1073.      * Executes all document deletions for documents of the specified type.
  1074.      *
  1075.      * @psalm-param ClassMetadata<T> $class
  1076.      * @psalm-param T[] $documents
  1077.      * @psalm-param CommitOptions $options
  1078.      *
  1079.      * @template T of object
  1080.      */
  1081.     private function executeDeletions(ClassMetadata $class, array $documents, array $options = []): void
  1082.     {
  1083.         $persister $this->getDocumentPersister($class->name);
  1084.         foreach ($documents as $oid => $document) {
  1085.             if (! $class->isEmbeddedDocument) {
  1086.                 $persister->delete($document$options);
  1087.             }
  1088.             unset(
  1089.                 $this->documentDeletions[$oid],
  1090.                 $this->documentIdentifiers[$oid],
  1091.                 $this->originalDocumentData[$oid]
  1092.             );
  1093.             // Clear snapshot information for any referenced PersistentCollection
  1094.             // http://www.doctrine-project.org/jira/browse/MODM-95
  1095.             foreach ($class->associationMappings as $fieldMapping) {
  1096.                 if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
  1097.                     continue;
  1098.                 }
  1099.                 $value $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
  1100.                 if (! ($value instanceof PersistentCollectionInterface)) {
  1101.                     continue;
  1102.                 }
  1103.                 $value->clearSnapshot();
  1104.             }
  1105.             // Document with this $oid after deletion treated as NEW, even if the $oid
  1106.             // is obtained by a new document because the old one went out of scope.
  1107.             $this->documentStates[$oid] = self::STATE_NEW;
  1108.             $this->lifecycleEventManager->postRemove($class$document);
  1109.         }
  1110.     }
  1111.     /**
  1112.      * Schedules a document for insertion into the database.
  1113.      * If the document already has an identifier, it will be added to the
  1114.      * identity map.
  1115.      *
  1116.      * @internal
  1117.      *
  1118.      * @psalm-param ClassMetadata<T> $class
  1119.      * @psalm-param T $document
  1120.      *
  1121.      * @throws InvalidArgumentException
  1122.      *
  1123.      * @template T of object
  1124.      */
  1125.     public function scheduleForInsert(ClassMetadata $classobject $document): void
  1126.     {
  1127.         $oid spl_object_hash($document);
  1128.         if (isset($this->documentUpdates[$oid])) {
  1129.             throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
  1130.         }
  1131.         if (isset($this->documentDeletions[$oid])) {
  1132.             throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
  1133.         }
  1134.         if (isset($this->documentInsertions[$oid])) {
  1135.             throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
  1136.         }
  1137.         $this->documentInsertions[$oid] = $document;
  1138.         if (! isset($this->documentIdentifiers[$oid])) {
  1139.             return;
  1140.         }
  1141.         $this->addToIdentityMap($document);
  1142.     }
  1143.     /**
  1144.      * Schedules a document for upsert into the database and adds it to the
  1145.      * identity map
  1146.      *
  1147.      * @internal
  1148.      *
  1149.      * @psalm-param ClassMetadata<T> $class
  1150.      * @psalm-param T $document
  1151.      *
  1152.      * @throws InvalidArgumentException
  1153.      *
  1154.      * @template T of object
  1155.      */
  1156.     public function scheduleForUpsert(ClassMetadata $classobject $document): void
  1157.     {
  1158.         $oid spl_object_hash($document);
  1159.         if ($class->isEmbeddedDocument) {
  1160.             throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
  1161.         }
  1162.         if (isset($this->documentUpdates[$oid])) {
  1163.             throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
  1164.         }
  1165.         if (isset($this->documentDeletions[$oid])) {
  1166.             throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
  1167.         }
  1168.         if (isset($this->documentUpserts[$oid])) {
  1169.             throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
  1170.         }
  1171.         $this->documentUpserts[$oid]     = $document;
  1172.         $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
  1173.         $this->addToIdentityMap($document);
  1174.     }
  1175.     /**
  1176.      * Checks whether a document is scheduled for insertion.
  1177.      */
  1178.     public function isScheduledForInsert(object $document): bool
  1179.     {
  1180.         return isset($this->documentInsertions[spl_object_hash($document)]);
  1181.     }
  1182.     /**
  1183.      * Checks whether a document is scheduled for upsert.
  1184.      */
  1185.     public function isScheduledForUpsert(object $document): bool
  1186.     {
  1187.         return isset($this->documentUpserts[spl_object_hash($document)]);
  1188.     }
  1189.     /**
  1190.      * Schedules a document for being updated.
  1191.      *
  1192.      * @internal
  1193.      *
  1194.      * @throws InvalidArgumentException
  1195.      */
  1196.     public function scheduleForUpdate(object $document): void
  1197.     {
  1198.         $oid spl_object_hash($document);
  1199.         if (! isset($this->documentIdentifiers[$oid])) {
  1200.             throw new InvalidArgumentException('Document has no identity.');
  1201.         }
  1202.         if (isset($this->documentDeletions[$oid])) {
  1203.             throw new InvalidArgumentException('Document is removed.');
  1204.         }
  1205.         if (
  1206.             isset($this->documentUpdates[$oid])
  1207.             || isset($this->documentInsertions[$oid])
  1208.             || isset($this->documentUpserts[$oid])
  1209.         ) {
  1210.             return;
  1211.         }
  1212.         $this->documentUpdates[$oid] = $document;
  1213.     }
  1214.     /**
  1215.      * Checks whether a document is registered as dirty in the unit of work.
  1216.      * Note: Is not very useful currently as dirty documents are only registered
  1217.      * at commit time.
  1218.      */
  1219.     public function isScheduledForUpdate(object $document): bool
  1220.     {
  1221.         return isset($this->documentUpdates[spl_object_hash($document)]);
  1222.     }
  1223.     /**
  1224.      * Checks whether a document is registered to be checked in the unit of work.
  1225.      */
  1226.     public function isScheduledForSynchronization(object $document): bool
  1227.     {
  1228.         $class $this->dm->getClassMetadata(get_class($document));
  1229.         return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
  1230.     }
  1231.     /**
  1232.      * Schedules a document for deletion.
  1233.      *
  1234.      * @internal
  1235.      */
  1236.     public function scheduleForDelete(object $documentbool $isView false): void
  1237.     {
  1238.         $oid spl_object_hash($document);
  1239.         if (isset($this->documentInsertions[$oid])) {
  1240.             if ($this->isInIdentityMap($document)) {
  1241.                 $this->removeFromIdentityMap($document);
  1242.             }
  1243.             unset($this->documentInsertions[$oid]);
  1244.             return; // document has not been persisted yet, so nothing more to do.
  1245.         }
  1246.         if (! $this->isInIdentityMap($document)) {
  1247.             return; // ignore
  1248.         }
  1249.         $this->removeFromIdentityMap($document);
  1250.         $this->documentStates[$oid] = self::STATE_REMOVED;
  1251.         if (isset($this->documentUpdates[$oid])) {
  1252.             unset($this->documentUpdates[$oid]);
  1253.         }
  1254.         if (isset($this->documentUpserts[$oid])) {
  1255.             unset($this->documentUpserts[$oid]);
  1256.         }
  1257.         if (isset($this->documentDeletions[$oid])) {
  1258.             return;
  1259.         }
  1260.         if ($isView) {
  1261.             return;
  1262.         }
  1263.         $this->documentDeletions[$oid] = $document;
  1264.     }
  1265.     /**
  1266.      * Checks whether a document is registered as removed/deleted with the unit
  1267.      * of work.
  1268.      */
  1269.     public function isScheduledForDelete(object $document): bool
  1270.     {
  1271.         return isset($this->documentDeletions[spl_object_hash($document)]);
  1272.     }
  1273.     /**
  1274.      * Checks whether a document is scheduled for insertion, update or deletion.
  1275.      *
  1276.      * @internal
  1277.      */
  1278.     public function isDocumentScheduled(object $document): bool
  1279.     {
  1280.         $oid spl_object_hash($document);
  1281.         return isset($this->documentInsertions[$oid]) ||
  1282.             isset($this->documentUpserts[$oid]) ||
  1283.             isset($this->documentUpdates[$oid]) ||
  1284.             isset($this->documentDeletions[$oid]);
  1285.     }
  1286.     /**
  1287.      * Registers a document in the identity map.
  1288.      *
  1289.      * Note that documents in a hierarchy are registered with the class name of
  1290.      * the root document. Identifiers are serialized before being used as array
  1291.      * keys to allow differentiation of equal, but not identical, values.
  1292.      *
  1293.      * @internal
  1294.      */
  1295.     public function addToIdentityMap(object $document): bool
  1296.     {
  1297.         $class $this->dm->getClassMetadata(get_class($document));
  1298.         $id    $this->getIdForIdentityMap($document);
  1299.         if (isset($this->identityMap[$class->name][$id])) {
  1300.             return false;
  1301.         }
  1302.         $this->identityMap[$class->name][$id] = $document;
  1303.         if (
  1304.             $document instanceof NotifyPropertyChanged &&
  1305.             ( ! $document instanceof GhostObjectInterface || $document->isProxyInitialized())
  1306.         ) {
  1307.             $document->addPropertyChangedListener($this);
  1308.         }
  1309.         return true;
  1310.     }
  1311.     /**
  1312.      * Gets the state of a document with regard to the current unit of work.
  1313.      *
  1314.      * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
  1315.      *                         This parameter can be set to improve performance of document state detection
  1316.      *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
  1317.      *                         is either known or does not matter for the caller of the method.
  1318.      */
  1319.     public function getDocumentState(object $document, ?int $assume null): int
  1320.     {
  1321.         $oid spl_object_hash($document);
  1322.         if (isset($this->documentStates[$oid])) {
  1323.             return $this->documentStates[$oid];
  1324.         }
  1325.         $class $this->dm->getClassMetadata(get_class($document));
  1326.         if ($class->isEmbeddedDocument) {
  1327.             return self::STATE_NEW;
  1328.         }
  1329.         if ($assume !== null) {
  1330.             return $assume;
  1331.         }
  1332.         /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
  1333.          * known. Note that you cannot remember the NEW or DETACHED state in
  1334.          * _documentStates since the UoW does not hold references to such
  1335.          * objects and the object hash can be reused. More generally, because
  1336.          * the state may "change" between NEW/DETACHED without the UoW being
  1337.          * aware of it.
  1338.          */
  1339.         $id $class->getIdentifierObject($document);
  1340.         if ($id === null) {
  1341.             return self::STATE_NEW;
  1342.         }
  1343.         // Check for a version field, if available, to avoid a DB lookup.
  1344.         if ($class->isVersioned && $class->versionField !== null) {
  1345.             return $class->getFieldValue($document$class->versionField)
  1346.                 ? self::STATE_DETACHED
  1347.                 self::STATE_NEW;
  1348.         }
  1349.         // Last try before DB lookup: check the identity map.
  1350.         if ($this->tryGetById($id$class)) {
  1351.             return self::STATE_DETACHED;
  1352.         }
  1353.         // DB lookup
  1354.         if ($this->getDocumentPersister($class->name)->exists($document)) {
  1355.             return self::STATE_DETACHED;
  1356.         }
  1357.         return self::STATE_NEW;
  1358.     }
  1359.     /**
  1360.      * Removes a document from the identity map. This effectively detaches the
  1361.      * document from the persistence management of Doctrine.
  1362.      *
  1363.      * @internal
  1364.      *
  1365.      * @throws InvalidArgumentException
  1366.      */
  1367.     public function removeFromIdentityMap(object $document): bool
  1368.     {
  1369.         $oid spl_object_hash($document);
  1370.         // Check if id is registered first
  1371.         if (! isset($this->documentIdentifiers[$oid])) {
  1372.             return false;
  1373.         }
  1374.         $class $this->dm->getClassMetadata(get_class($document));
  1375.         $id    $this->getIdForIdentityMap($document);
  1376.         if (isset($this->identityMap[$class->name][$id])) {
  1377.             unset($this->identityMap[$class->name][$id]);
  1378.             $this->documentStates[$oid] = self::STATE_DETACHED;
  1379.             return true;
  1380.         }
  1381.         return false;
  1382.     }
  1383.     /**
  1384.      * Gets a document in the identity map by its identifier hash.
  1385.      *
  1386.      * @internal
  1387.      *
  1388.      * @param mixed $id Document identifier
  1389.      * @psalm-param ClassMetadata<T> $class
  1390.      *
  1391.      * @psalm-return T
  1392.      *
  1393.      * @throws InvalidArgumentException If the class does not have an identifier.
  1394.      *
  1395.      * @template T of object
  1396.      *
  1397.      * @psalm-suppress InvalidReturnStatement, InvalidReturnType because of the inability of defining a generic property map
  1398.      */
  1399.     public function getById($idClassMetadata $class): object
  1400.     {
  1401.         if (! $class->identifier) {
  1402.             throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier'$class->name));
  1403.         }
  1404.         $serializedId serialize($class->getDatabaseIdentifierValue($id));
  1405.         return $this->identityMap[$class->name][$serializedId];
  1406.     }
  1407.     /**
  1408.      * Tries to get a document by its identifier hash. If no document is found
  1409.      * for the given hash, FALSE is returned.
  1410.      *
  1411.      * @internal
  1412.      *
  1413.      * @param mixed $id Document identifier
  1414.      * @psalm-param ClassMetadata<T> $class
  1415.      *
  1416.      * @return mixed The found document or FALSE.
  1417.      * @psalm-return T|false
  1418.      *
  1419.      * @throws InvalidArgumentException If the class does not have an identifier.
  1420.      *
  1421.      * @template T of object
  1422.      *
  1423.      * @psalm-suppress InvalidReturnStatement, InvalidReturnType because of the inability of defining a generic property map
  1424.      */
  1425.     public function tryGetById($idClassMetadata $class)
  1426.     {
  1427.         if (! $class->identifier) {
  1428.             throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier'$class->name));
  1429.         }
  1430.         $serializedId serialize($class->getDatabaseIdentifierValue($id));
  1431.         return $this->identityMap[$class->name][$serializedId] ?? false;
  1432.     }
  1433.     /**
  1434.      * Schedules a document for dirty-checking at commit-time.
  1435.      *
  1436.      * @internal
  1437.      */
  1438.     public function scheduleForSynchronization(object $document): void
  1439.     {
  1440.         $class                                                                       $this->dm->getClassMetadata(get_class($document));
  1441.         $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
  1442.     }
  1443.     /**
  1444.      * Checks whether a document is registered in the identity map.
  1445.      *
  1446.      * @internal
  1447.      */
  1448.     public function isInIdentityMap(object $document): bool
  1449.     {
  1450.         $oid spl_object_hash($document);
  1451.         if (! isset($this->documentIdentifiers[$oid])) {
  1452.             return false;
  1453.         }
  1454.         $class $this->dm->getClassMetadata(get_class($document));
  1455.         $id    $this->getIdForIdentityMap($document);
  1456.         return isset($this->identityMap[$class->name][$id]);
  1457.     }
  1458.     private function getIdForIdentityMap(object $document): string
  1459.     {
  1460.         $class $this->dm->getClassMetadata(get_class($document));
  1461.         if (! $class->identifier) {
  1462.             $id spl_object_hash($document);
  1463.         } else {
  1464.             $id $this->documentIdentifiers[spl_object_hash($document)];
  1465.             $id serialize($class->getDatabaseIdentifierValue($id));
  1466.         }
  1467.         return $id;
  1468.     }
  1469.     /**
  1470.      * Checks whether an identifier exists in the identity map.
  1471.      *
  1472.      * @internal
  1473.      *
  1474.      * @param mixed $id
  1475.      */
  1476.     public function containsId($idstring $rootClassName): bool
  1477.     {
  1478.         return isset($this->identityMap[$rootClassName][serialize($id)]);
  1479.     }
  1480.     /**
  1481.      * Persists a document as part of the current unit of work.
  1482.      *
  1483.      * @internal
  1484.      *
  1485.      * @throws MongoDBException If trying to persist MappedSuperclass.
  1486.      * @throws InvalidArgumentException If there is something wrong with document's identifier.
  1487.      */
  1488.     public function persist(object $document): void
  1489.     {
  1490.         $class $this->dm->getClassMetadata(get_class($document));
  1491.         if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
  1492.             throw MongoDBException::cannotPersistMappedSuperclass($class->name);
  1493.         }
  1494.         $visited = [];
  1495.         $this->doPersist($document$visited);
  1496.     }
  1497.     /**
  1498.      * Saves a document as part of the current unit of work.
  1499.      * This method is internally called during save() cascades as it tracks
  1500.      * the already visited documents to prevent infinite recursions.
  1501.      *
  1502.      * NOTE: This method always considers documents that are not yet known to
  1503.      * this UnitOfWork as NEW.
  1504.      *
  1505.      * @param array<string, object> $visited
  1506.      *
  1507.      * @throws InvalidArgumentException
  1508.      * @throws MongoDBException
  1509.      */
  1510.     private function doPersist(object $document, array &$visited): void
  1511.     {
  1512.         $oid spl_object_hash($document);
  1513.         if (isset($visited[$oid])) {
  1514.             return; // Prevent infinite recursion
  1515.         }
  1516.         $visited[$oid] = $document// Mark visited
  1517.         $class $this->dm->getClassMetadata(get_class($document));
  1518.         $documentState $this->getDocumentState($documentself::STATE_NEW);
  1519.         switch ($documentState) {
  1520.             case self::STATE_MANAGED:
  1521.                 // Nothing to do, except if policy is "deferred explicit"
  1522.                 if ($class->isChangeTrackingDeferredExplicit() && ! $class->isView()) {
  1523.                     $this->scheduleForSynchronization($document);
  1524.                 }
  1525.                 break;
  1526.             case self::STATE_NEW:
  1527.                 if ($class->isFile) {
  1528.                     throw MongoDBException::cannotPersistGridFSFile($class->name);
  1529.                 }
  1530.                 if ($class->isView()) {
  1531.                     return;
  1532.                 }
  1533.                 $this->persistNew($class$document);
  1534.                 break;
  1535.             case self::STATE_REMOVED:
  1536.                 // Document becomes managed again
  1537.                 unset($this->documentDeletions[$oid]);
  1538.                 $this->documentStates[$oid] = self::STATE_MANAGED;
  1539.                 break;
  1540.             case self::STATE_DETACHED:
  1541.                 throw new InvalidArgumentException(
  1542.                     'Behavior of persist() for a detached document is not yet defined.'
  1543.                 );
  1544.             default:
  1545.                 throw MongoDBException::invalidDocumentState($documentState);
  1546.         }
  1547.         $this->cascadePersist($document$visited);
  1548.     }
  1549.     /**
  1550.      * Deletes a document as part of the current unit of work.
  1551.      *
  1552.      * @internal
  1553.      */
  1554.     public function remove(object $document): void
  1555.     {
  1556.         $visited = [];
  1557.         $this->doRemove($document$visited);
  1558.     }
  1559.     /**
  1560.      * Deletes a document as part of the current unit of work.
  1561.      *
  1562.      * This method is internally called during delete() cascades as it tracks
  1563.      * the already visited documents to prevent infinite recursions.
  1564.      *
  1565.      * @param array<string, object> $visited
  1566.      *
  1567.      * @throws MongoDBException
  1568.      */
  1569.     private function doRemove(object $document, array &$visited): void
  1570.     {
  1571.         $oid spl_object_hash($document);
  1572.         if (isset($visited[$oid])) {
  1573.             return; // Prevent infinite recursion
  1574.         }
  1575.         $visited[$oid] = $document// mark visited
  1576.         /* Cascade first, because scheduleForDelete() removes the entity from
  1577.          * the identity map, which can cause problems when a lazy Proxy has to
  1578.          * be initialized for the cascade operation.
  1579.          */
  1580.         $this->cascadeRemove($document$visited);
  1581.         $class         $this->dm->getClassMetadata(get_class($document));
  1582.         $documentState $this->getDocumentState($document);
  1583.         switch ($documentState) {
  1584.             case self::STATE_NEW:
  1585.             case self::STATE_REMOVED:
  1586.                 // nothing to do
  1587.                 break;
  1588.             case self::STATE_MANAGED:
  1589.                 $this->lifecycleEventManager->preRemove($class$document);
  1590.                 $this->scheduleForDelete($document$class->isView());
  1591.                 break;
  1592.             case self::STATE_DETACHED:
  1593.                 throw MongoDBException::detachedDocumentCannotBeRemoved();
  1594.             default:
  1595.                 throw MongoDBException::invalidDocumentState($documentState);
  1596.         }
  1597.     }
  1598.     /**
  1599.      * Merges the state of the given detached document into this UnitOfWork.
  1600.      *
  1601.      * @internal
  1602.      */
  1603.     public function merge(object $document): object
  1604.     {
  1605.         $visited = [];
  1606.         return $this->doMerge($document$visited);
  1607.     }
  1608.     /**
  1609.      * Executes a merge operation on a document.
  1610.      *
  1611.      * @param array<string, object> $visited
  1612.      * @psalm-param AssociationFieldMapping|null $assoc
  1613.      *
  1614.      * @throws InvalidArgumentException If the entity instance is NEW.
  1615.      * @throws LockException If the document uses optimistic locking through a
  1616.      *                       version attribute and the version check against the
  1617.      *                       managed copy fails.
  1618.      */
  1619.     private function doMerge(object $document, array &$visited, ?object $prevManagedCopy null, ?array $assoc null): object
  1620.     {
  1621.         $oid spl_object_hash($document);
  1622.         if (isset($visited[$oid])) {
  1623.             return $visited[$oid]; // Prevent infinite recursion
  1624.         }
  1625.         $visited[$oid] = $document// mark visited
  1626.         $class $this->dm->getClassMetadata(get_class($document));
  1627.         /* First we assume DETACHED, although it can still be NEW but we can
  1628.          * avoid an extra DB round trip this way. If it is not MANAGED but has
  1629.          * an identity, we need to fetch it from the DB anyway in order to
  1630.          * merge. MANAGED documents are ignored by the merge operation.
  1631.          */
  1632.         $managedCopy $document;
  1633.         if ($this->getDocumentState($documentself::STATE_DETACHED) !== self::STATE_MANAGED) {
  1634.             if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  1635.                 $document->initializeProxy();
  1636.             }
  1637.             $identifier $class->getIdentifier();
  1638.             // We always have one element in the identifier array but it might be null
  1639.             $id          $identifier[0] !== null $class->getIdentifierObject($document) : null;
  1640.             $managedCopy null;
  1641.             // Try to fetch document from the database
  1642.             if (! $class->isEmbeddedDocument && $id !== null) {
  1643.                 $managedCopy $this->dm->find($class->name$id);
  1644.                 // Managed copy may be removed in which case we can't merge
  1645.                 if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
  1646.                     throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
  1647.                 }
  1648.                 if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
  1649.                     $managedCopy->initializeProxy();
  1650.                 }
  1651.             }
  1652.             if ($managedCopy === null) {
  1653.                 // Create a new managed instance
  1654.                 $managedCopy $class->newInstance();
  1655.                 if ($id !== null) {
  1656.                     $class->setIdentifierValue($managedCopy$id);
  1657.                 }
  1658.                 $this->persistNew($class$managedCopy);
  1659.             }
  1660.             if ($class->isVersioned) {
  1661.                 $managedCopyVersion $class->reflFields[$class->versionField]->getValue($managedCopy);
  1662.                 $documentVersion    $class->reflFields[$class->versionField]->getValue($document);
  1663.                 // Throw exception if versions don't match
  1664.                 if ($managedCopyVersion !== $documentVersion) {
  1665.                     throw LockException::lockFailedVersionMissmatch($document$documentVersion$managedCopyVersion);
  1666.                 }
  1667.             }
  1668.             // Merge state of $document into existing (managed) document
  1669.             foreach ($class->reflClass->getProperties() as $nativeReflection) {
  1670.                 $name $nativeReflection->name;
  1671.                 $prop $this->reflectionService->getAccessibleProperty($class->name$name);
  1672.                 assert($prop instanceof ReflectionProperty);
  1673.                 if (method_exists($prop'isInitialized') && ! $prop->isInitialized($document)) {
  1674.                     continue;
  1675.                 }
  1676.                 if (! isset($class->associationMappings[$name])) {
  1677.                     if (! $class->isIdentifier($name)) {
  1678.                         $prop->setValue($managedCopy$prop->getValue($document));
  1679.                     }
  1680.                 } else {
  1681.                     $assoc2 $class->associationMappings[$name];
  1682.                     if ($assoc2['type'] === ClassMetadata::ONE) {
  1683.                         $other $prop->getValue($document);
  1684.                         if ($other === null) {
  1685.                             $prop->setValue($managedCopynull);
  1686.                         } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
  1687.                             // Do not merge fields marked lazy that have not been fetched
  1688.                             continue;
  1689.                         } elseif (! $assoc2['isCascadeMerge']) {
  1690.                             if ($this->getDocumentState($other) === self::STATE_DETACHED) {
  1691.                                 $targetDocument $assoc2['targetDocument'] ?? get_class($other);
  1692.                                 $targetClass    $this->dm->getClassMetadata($targetDocument);
  1693.                                 assert($targetClass instanceof ClassMetadata);
  1694.                                 $relatedId $targetClass->getIdentifierObject($other);
  1695.                                 $current $prop->getValue($managedCopy);
  1696.                                 if ($current !== null) {
  1697.                                     $this->removeFromIdentityMap($current);
  1698.                                 }
  1699.                                 if ($targetClass->subClasses) {
  1700.                                     $other $this->dm->find($targetClass->name$relatedId);
  1701.                                 } else {
  1702.                                     $other $this
  1703.                                         ->dm
  1704.                                         ->getProxyFactory()
  1705.                                         ->getProxy($targetClass$relatedId);
  1706.                                     $this->registerManaged($other$relatedId, []);
  1707.                                 }
  1708.                             }
  1709.                             $prop->setValue($managedCopy$other);
  1710.                         }
  1711.                     } else {
  1712.                         $mergeCol $prop->getValue($document);
  1713.                         if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
  1714.                             /* Do not merge fields marked lazy that have not
  1715.                              * been fetched. Keep the lazy persistent collection
  1716.                              * of the managed copy.
  1717.                              */
  1718.                             continue;
  1719.                         }
  1720.                         $managedCol $prop->getValue($managedCopy);
  1721.                         if (! $managedCol) {
  1722.                             $managedCol $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm$assoc2null);
  1723.                             $managedCol->setOwner($managedCopy$assoc2);
  1724.                             $prop->setValue($managedCopy$managedCol);
  1725.                             $this->originalDocumentData[$oid][$name] = $managedCol;
  1726.                         }
  1727.                         /* Note: do not process association's target documents.
  1728.                          * They will be handled during the cascade. Initialize
  1729.                          * and, if necessary, clear $managedCol for now.
  1730.                          */
  1731.                         if ($assoc2['isCascadeMerge']) {
  1732.                             $managedCol->initialize();
  1733.                             // If $managedCol differs from the merged collection, clear and set dirty
  1734.                             if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
  1735.                                 $managedCol->unwrap()->clear();
  1736.                                 $managedCol->setDirty(true);
  1737.                                 if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
  1738.                                     $this->scheduleForSynchronization($managedCopy);
  1739.                                 }
  1740.                             }
  1741.                         }
  1742.                     }
  1743.                 }
  1744.                 if (! $class->isChangeTrackingNotify()) {
  1745.                     continue;
  1746.                 }
  1747.                 // Just treat all properties as changed, there is no other choice.
  1748.                 $this->propertyChanged($managedCopy$namenull$prop->getValue($managedCopy));
  1749.             }
  1750.             if ($class->isChangeTrackingDeferredExplicit()) {
  1751.                 $this->scheduleForSynchronization($document);
  1752.             }
  1753.         }
  1754.         if ($prevManagedCopy !== null) {
  1755.             $assocField $assoc['fieldName'];
  1756.             $prevClass  $this->dm->getClassMetadata(get_class($prevManagedCopy));
  1757.             if ($assoc['type'] === ClassMetadata::ONE) {
  1758.                 $prevClass->reflFields[$assocField]->setValue($prevManagedCopy$managedCopy);
  1759.             } else {
  1760.                 $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
  1761.                 if ($assoc['type'] === ClassMetadata::MANY && isset($assoc['mappedBy'])) {
  1762.                     $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy$prevManagedCopy);
  1763.                 }
  1764.             }
  1765.         }
  1766.         // Mark the managed copy visited as well
  1767.         $visited[spl_object_hash($managedCopy)] = $managedCopy;
  1768.         $this->cascadeMerge($document$managedCopy$visited);
  1769.         return $managedCopy;
  1770.     }
  1771.     /**
  1772.      * Detaches a document from the persistence management. It's persistence will
  1773.      * no longer be managed by Doctrine.
  1774.      *
  1775.      * @internal
  1776.      */
  1777.     public function detach(object $document): void
  1778.     {
  1779.         $visited = [];
  1780.         $this->doDetach($document$visited);
  1781.     }
  1782.     /**
  1783.      * Executes a detach operation on the given document.
  1784.      *
  1785.      * @param array<string, object> $visited
  1786.      */
  1787.     private function doDetach(object $document, array &$visited): void
  1788.     {
  1789.         $oid spl_object_hash($document);
  1790.         if (isset($visited[$oid])) {
  1791.             return; // Prevent infinite recursion
  1792.         }
  1793.         $visited[$oid] = $document// mark visited
  1794.         switch ($this->getDocumentState($documentself::STATE_DETACHED)) {
  1795.             case self::STATE_MANAGED:
  1796.                 $this->removeFromIdentityMap($document);
  1797.                 unset(
  1798.                     $this->documentInsertions[$oid],
  1799.                     $this->documentUpdates[$oid],
  1800.                     $this->documentDeletions[$oid],
  1801.                     $this->documentIdentifiers[$oid],
  1802.                     $this->documentStates[$oid],
  1803.                     $this->originalDocumentData[$oid],
  1804.                     $this->parentAssociations[$oid],
  1805.                     $this->documentUpserts[$oid],
  1806.                     $this->hasScheduledCollections[$oid],
  1807.                     $this->embeddedDocumentsRegistry[$oid]
  1808.                 );
  1809.                 break;
  1810.             case self::STATE_NEW:
  1811.             case self::STATE_DETACHED:
  1812.                 return;
  1813.         }
  1814.         $this->cascadeDetach($document$visited);
  1815.     }
  1816.     /**
  1817.      * Refreshes the state of the given document from the database, overwriting
  1818.      * any local, unpersisted changes.
  1819.      *
  1820.      * @internal
  1821.      *
  1822.      * @throws InvalidArgumentException If the document is not MANAGED.
  1823.      */
  1824.     public function refresh(object $document): void
  1825.     {
  1826.         $visited = [];
  1827.         $this->doRefresh($document$visited);
  1828.     }
  1829.     /**
  1830.      * Executes a refresh operation on a document.
  1831.      *
  1832.      * @param array<string, object> $visited
  1833.      *
  1834.      * @throws InvalidArgumentException If the document is not MANAGED.
  1835.      */
  1836.     private function doRefresh(object $document, array &$visited): void
  1837.     {
  1838.         $oid spl_object_hash($document);
  1839.         if (isset($visited[$oid])) {
  1840.             return; // Prevent infinite recursion
  1841.         }
  1842.         $visited[$oid] = $document// mark visited
  1843.         $class $this->dm->getClassMetadata(get_class($document));
  1844.         if (! $class->isEmbeddedDocument) {
  1845.             if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
  1846.                 throw new InvalidArgumentException('Document is not MANAGED.');
  1847.             }
  1848.             $this->getDocumentPersister($class->name)->refresh($document);
  1849.         }
  1850.         $this->cascadeRefresh($document$visited);
  1851.     }
  1852.     /**
  1853.      * Cascades a refresh operation to associated documents.
  1854.      *
  1855.      * @param array<string, object> $visited
  1856.      */
  1857.     private function cascadeRefresh(object $document, array &$visited): void
  1858.     {
  1859.         $class $this->dm->getClassMetadata(get_class($document));
  1860.         $associationMappings array_filter(
  1861.             $class->associationMappings,
  1862.             static function ($assoc) {
  1863.                 return $assoc['isCascadeRefresh'];
  1864.             }
  1865.         );
  1866.         foreach ($associationMappings as $mapping) {
  1867.             $relatedDocuments $class->reflFields[$mapping['fieldName']]->getValue($document);
  1868.             if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
  1869.                 if ($relatedDocuments instanceof PersistentCollectionInterface) {
  1870.                     // Unwrap so that foreach() does not initialize
  1871.                     $relatedDocuments $relatedDocuments->unwrap();
  1872.                 }
  1873.                 foreach ($relatedDocuments as $relatedDocument) {
  1874.                     $this->doRefresh($relatedDocument$visited);
  1875.                 }
  1876.             } elseif ($relatedDocuments !== null) {
  1877.                 $this->doRefresh($relatedDocuments$visited);
  1878.             }
  1879.         }
  1880.     }
  1881.     /**
  1882.      * Cascades a detach operation to associated documents.
  1883.      *
  1884.      * @param array<string, object> $visited
  1885.      */
  1886.     private function cascadeDetach(object $document, array &$visited): void
  1887.     {
  1888.         $class $this->dm->getClassMetadata(get_class($document));
  1889.         foreach ($class->fieldMappings as $mapping) {
  1890.             if (! $mapping['isCascadeDetach']) {
  1891.                 continue;
  1892.             }
  1893.             $relatedDocuments $class->reflFields[$mapping['fieldName']]->getValue($document);
  1894.             if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
  1895.                 if ($relatedDocuments instanceof PersistentCollectionInterface) {
  1896.                     // Unwrap so that foreach() does not initialize
  1897.                     $relatedDocuments $relatedDocuments->unwrap();
  1898.                 }
  1899.                 foreach ($relatedDocuments as $relatedDocument) {
  1900.                     $this->doDetach($relatedDocument$visited);
  1901.                 }
  1902.             } elseif ($relatedDocuments !== null) {
  1903.                 $this->doDetach($relatedDocuments$visited);
  1904.             }
  1905.         }
  1906.     }
  1907.     /**
  1908.      * Cascades a merge operation to associated documents.
  1909.      *
  1910.      * @param array<string, object> $visited
  1911.      */
  1912.     private function cascadeMerge(object $documentobject $managedCopy, array &$visited): void
  1913.     {
  1914.         $class $this->dm->getClassMetadata(get_class($document));
  1915.         $associationMappings array_filter(
  1916.             $class->associationMappings,
  1917.             static function ($assoc) {
  1918.                 return $assoc['isCascadeMerge'];
  1919.             }
  1920.         );
  1921.         foreach ($associationMappings as $assoc) {
  1922.             $relatedDocuments $class->reflFields[$assoc['fieldName']]->getValue($document);
  1923.             if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
  1924.                 if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
  1925.                     // Collections are the same, so there is nothing to do
  1926.                     continue;
  1927.                 }
  1928.                 foreach ($relatedDocuments as $relatedDocument) {
  1929.                     $this->doMerge($relatedDocument$visited$managedCopy$assoc);
  1930.                 }
  1931.             } elseif ($relatedDocuments !== null) {
  1932.                 $this->doMerge($relatedDocuments$visited$managedCopy$assoc);
  1933.             }
  1934.         }
  1935.     }
  1936.     /**
  1937.      * Cascades the save operation to associated documents.
  1938.      *
  1939.      * @param array<string, object> $visited
  1940.      */
  1941.     private function cascadePersist(object $document, array &$visited): void
  1942.     {
  1943.         $class $this->dm->getClassMetadata(get_class($document));
  1944.         $associationMappings array_filter(
  1945.             $class->associationMappings,
  1946.             static function ($assoc) {
  1947.                 return $assoc['isCascadePersist'];
  1948.             }
  1949.         );
  1950.         foreach ($associationMappings as $fieldName => $mapping) {
  1951.             $relatedDocuments $class->reflFields[$fieldName]->getValue($document);
  1952.             if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
  1953.                 if ($relatedDocuments instanceof PersistentCollectionInterface) {
  1954.                     if ($relatedDocuments->getOwner() !== $document) {
  1955.                         $relatedDocuments $this->fixPersistentCollectionOwnership($relatedDocuments$document$class$mapping['fieldName']);
  1956.                     }
  1957.                     // Unwrap so that foreach() does not initialize
  1958.                     $relatedDocuments $relatedDocuments->unwrap();
  1959.                 }
  1960.                 $count 0;
  1961.                 foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
  1962.                     if (! empty($mapping['embedded'])) {
  1963.                         [, $knownParent] = $this->getParentAssociation($relatedDocument);
  1964.                         if ($knownParent && $knownParent !== $document) {
  1965.                             $relatedDocument               = clone $relatedDocument;
  1966.                             $relatedDocuments[$relatedKey] = $relatedDocument;
  1967.                         }
  1968.                         $pathKey CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
  1969.                         $this->setParentAssociation($relatedDocument$mapping$document$mapping['fieldName'] . '.' $pathKey);
  1970.                     }
  1971.                     $this->doPersist($relatedDocument$visited);
  1972.                 }
  1973.             } elseif ($relatedDocuments !== null) {
  1974.                 if (! empty($mapping['embedded'])) {
  1975.                     [, $knownParent] = $this->getParentAssociation($relatedDocuments);
  1976.                     if ($knownParent && $knownParent !== $document) {
  1977.                         $relatedDocuments = clone $relatedDocuments;
  1978.                         $class->setFieldValue($document$mapping['fieldName'], $relatedDocuments);
  1979.                     }
  1980.                     $this->setParentAssociation($relatedDocuments$mapping$document$mapping['fieldName']);
  1981.                 }
  1982.                 $this->doPersist($relatedDocuments$visited);
  1983.             }
  1984.         }
  1985.     }
  1986.     /**
  1987.      * Cascades the delete operation to associated documents.
  1988.      *
  1989.      * @param array<string, object> $visited
  1990.      */
  1991.     private function cascadeRemove(object $document, array &$visited): void
  1992.     {
  1993.         $class $this->dm->getClassMetadata(get_class($document));
  1994.         foreach ($class->fieldMappings as $mapping) {
  1995.             if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
  1996.                 continue;
  1997.             }
  1998.             if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  1999.                 $document->initializeProxy();
  2000.             }
  2001.             $relatedDocuments $class->reflFields[$mapping['fieldName']]->getValue($document);
  2002.             if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
  2003.                 // If its a PersistentCollection initialization is intended! No unwrap!
  2004.                 foreach ($relatedDocuments as $relatedDocument) {
  2005.                     $this->doRemove($relatedDocument$visited);
  2006.                 }
  2007.             } elseif ($relatedDocuments !== null) {
  2008.                 $this->doRemove($relatedDocuments$visited);
  2009.             }
  2010.         }
  2011.     }
  2012.     /**
  2013.      * Acquire a lock on the given document.
  2014.      *
  2015.      * @internal
  2016.      *
  2017.      * @throws LockException
  2018.      * @throws InvalidArgumentException
  2019.      */
  2020.     public function lock(object $documentint $lockMode, ?int $lockVersion null): void
  2021.     {
  2022.         if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
  2023.             throw new InvalidArgumentException('Document is not MANAGED.');
  2024.         }
  2025.         $documentName get_class($document);
  2026.         $class        $this->dm->getClassMetadata($documentName);
  2027.         if ($lockMode === LockMode::OPTIMISTIC) {
  2028.             if (! $class->isVersioned) {
  2029.                 throw LockException::notVersioned($documentName);
  2030.             }
  2031.             if ($lockVersion !== null) {
  2032.                 $documentVersion $class->reflFields[$class->versionField]->getValue($document);
  2033.                 if ($documentVersion !== $lockVersion) {
  2034.                     throw LockException::lockFailedVersionMissmatch($document$lockVersion$documentVersion);
  2035.                 }
  2036.             }
  2037.         } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READLockMode::PESSIMISTIC_WRITE])) {
  2038.             $this->getDocumentPersister($class->name)->lock($document$lockMode);
  2039.         }
  2040.     }
  2041.     /**
  2042.      * Releases a lock on the given document.
  2043.      *
  2044.      * @internal
  2045.      *
  2046.      * @throws InvalidArgumentException
  2047.      */
  2048.     public function unlock(object $document): void
  2049.     {
  2050.         if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
  2051.             throw new InvalidArgumentException('Document is not MANAGED.');
  2052.         }
  2053.         $documentName get_class($document);
  2054.         $this->getDocumentPersister($documentName)->unlock($document);
  2055.     }
  2056.     /**
  2057.      * Clears the UnitOfWork.
  2058.      *
  2059.      * @internal
  2060.      */
  2061.     public function clear(?string $documentName null): void
  2062.     {
  2063.         if ($documentName === null) {
  2064.             $this->identityMap                 =
  2065.             $this->documentIdentifiers         =
  2066.             $this->originalDocumentData        =
  2067.             $this->documentChangeSets          =
  2068.             $this->documentStates              =
  2069.             $this->scheduledForSynchronization =
  2070.             $this->documentInsertions          =
  2071.             $this->documentUpserts             =
  2072.             $this->documentUpdates             =
  2073.             $this->documentDeletions           =
  2074.             $this->collectionUpdates           =
  2075.             $this->collectionDeletions         =
  2076.             $this->parentAssociations          =
  2077.             $this->embeddedDocumentsRegistry   =
  2078.             $this->orphanRemovals              =
  2079.             $this->hasScheduledCollections     = [];
  2080.         } else {
  2081.             $visited = [];
  2082.             foreach ($this->identityMap as $className => $documents) {
  2083.                 if ($className !== $documentName) {
  2084.                     continue;
  2085.                 }
  2086.                 foreach ($documents as $document) {
  2087.                     $this->doDetach($document$visited);
  2088.                 }
  2089.             }
  2090.         }
  2091.         if (! $this->evm->hasListeners(Events::onClear)) {
  2092.             return;
  2093.         }
  2094.         $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm$documentName));
  2095.     }
  2096.     /**
  2097.      * Schedules an embedded document for removal. The remove() operation will be
  2098.      * invoked on that document at the beginning of the next commit of this
  2099.      * UnitOfWork.
  2100.      *
  2101.      * @internal
  2102.      */
  2103.     public function scheduleOrphanRemoval(object $document): void
  2104.     {
  2105.         $this->orphanRemovals[spl_object_hash($document)] = $document;
  2106.     }
  2107.     /**
  2108.      * Unschedules an embedded or referenced object for removal.
  2109.      *
  2110.      * @internal
  2111.      */
  2112.     public function unscheduleOrphanRemoval(object $document): void
  2113.     {
  2114.         $oid spl_object_hash($document);
  2115.         unset($this->orphanRemovals[$oid]);
  2116.     }
  2117.     /**
  2118.      * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
  2119.      *  1) sets owner if it was cloned
  2120.      *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
  2121.      *  3) NOP if state is OK
  2122.      * Returned collection should be used from now on (only important with 2nd point)
  2123.      *
  2124.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2125.      * @psalm-param T $document
  2126.      * @psalm-param ClassMetadata<T> $class
  2127.      *
  2128.      * @psalm-return PersistentCollectionInterface<array-key, object>
  2129.      *
  2130.      * @template T of object
  2131.      */
  2132.     private function fixPersistentCollectionOwnership(PersistentCollectionInterface $collobject $documentClassMetadata $classstring $propName): PersistentCollectionInterface
  2133.     {
  2134.         $owner $coll->getOwner();
  2135.         if ($owner === null) { // cloned
  2136.             $coll->setOwner($document$class->fieldMappings[$propName]);
  2137.         } elseif ($owner !== $document) { // no clone, we have to fix
  2138.             if (! $coll->isInitialized()) {
  2139.                 $coll->initialize(); // we have to do this otherwise the cols share state
  2140.             }
  2141.             $newValue = clone $coll;
  2142.             $newValue->setOwner($document$class->fieldMappings[$propName]);
  2143.             $class->reflFields[$propName]->setValue($document$newValue);
  2144.             if ($this->isScheduledForUpdate($document)) {
  2145.                 // @todo following line should be superfluous once collections are stored in change sets
  2146.                 $this->setOriginalDocumentProperty(spl_object_hash($document), $propName$newValue);
  2147.             }
  2148.             return $newValue;
  2149.         }
  2150.         return $coll;
  2151.     }
  2152.     /**
  2153.      * Schedules a complete collection for removal when this UnitOfWork commits.
  2154.      *
  2155.      * @internal
  2156.      *
  2157.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2158.      */
  2159.     public function scheduleCollectionDeletion(PersistentCollectionInterface $coll): void
  2160.     {
  2161.         $oid spl_object_hash($coll);
  2162.         unset($this->collectionUpdates[$oid]);
  2163.         if (isset($this->collectionDeletions[$oid])) {
  2164.             return;
  2165.         }
  2166.         $this->collectionDeletions[$oid] = $coll;
  2167.         $this->scheduleCollectionOwner($coll);
  2168.     }
  2169.     /**
  2170.      * Checks whether a PersistentCollection is scheduled for deletion.
  2171.      *
  2172.      * @internal
  2173.      *
  2174.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2175.      */
  2176.     public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll): bool
  2177.     {
  2178.         return isset($this->collectionDeletions[spl_object_hash($coll)]);
  2179.     }
  2180.     /**
  2181.      * Unschedules a collection from being deleted when this UnitOfWork commits.
  2182.      *
  2183.      * @internal
  2184.      *
  2185.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2186.      */
  2187.     public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll): void
  2188.     {
  2189.         if ($coll->getOwner() === null) {
  2190.             return;
  2191.         }
  2192.         $oid spl_object_hash($coll);
  2193.         if (! isset($this->collectionDeletions[$oid])) {
  2194.             return;
  2195.         }
  2196.         $topmostOwner $this->getOwningDocument($coll->getOwner());
  2197.         unset($this->collectionDeletions[$oid]);
  2198.         unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
  2199.     }
  2200.     /**
  2201.      * Schedules a collection for update when this UnitOfWork commits.
  2202.      *
  2203.      * @internal
  2204.      *
  2205.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2206.      */
  2207.     public function scheduleCollectionUpdate(PersistentCollectionInterface $coll): void
  2208.     {
  2209.         $mapping $coll->getMapping();
  2210.         if (CollectionHelper::usesSet($mapping['strategy'])) {
  2211.             /* There is no need to $unset collection if it will be $set later
  2212.              * This is NOP if collection is not scheduled for deletion
  2213.              */
  2214.             $this->unscheduleCollectionDeletion($coll);
  2215.         }
  2216.         $oid spl_object_hash($coll);
  2217.         if (isset($this->collectionUpdates[$oid])) {
  2218.             return;
  2219.         }
  2220.         $this->collectionUpdates[$oid] = $coll;
  2221.         $this->scheduleCollectionOwner($coll);
  2222.     }
  2223.     /**
  2224.      * Unschedules a collection from being updated when this UnitOfWork commits.
  2225.      *
  2226.      * @internal
  2227.      *
  2228.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2229.      */
  2230.     public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll): void
  2231.     {
  2232.         if ($coll->getOwner() === null) {
  2233.             return;
  2234.         }
  2235.         $oid spl_object_hash($coll);
  2236.         if (! isset($this->collectionUpdates[$oid])) {
  2237.             return;
  2238.         }
  2239.         $topmostOwner $this->getOwningDocument($coll->getOwner());
  2240.         unset($this->collectionUpdates[$oid]);
  2241.         unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
  2242.     }
  2243.     /**
  2244.      * Checks whether a PersistentCollection is scheduled for update.
  2245.      *
  2246.      * @internal
  2247.      *
  2248.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2249.      */
  2250.     public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll): bool
  2251.     {
  2252.         return isset($this->collectionUpdates[spl_object_hash($coll)]);
  2253.     }
  2254.     /**
  2255.      * Gets PersistentCollections that have been visited during computing change
  2256.      * set of $document
  2257.      *
  2258.      * @internal
  2259.      *
  2260.      * @return PersistentCollectionInterface[]
  2261.      * @psalm-return array<PersistentCollectionInterface<array-key, object>>
  2262.      */
  2263.     public function getVisitedCollections(object $document): array
  2264.     {
  2265.         $oid spl_object_hash($document);
  2266.         return $this->visitedCollections[$oid] ?? [];
  2267.     }
  2268.     /**
  2269.      * Gets PersistentCollections that are scheduled to update and related to $document
  2270.      *
  2271.      * @internal
  2272.      *
  2273.      * @return PersistentCollectionInterface[]
  2274.      * @psalm-return array<string, PersistentCollectionInterface<array-key, object>>
  2275.      */
  2276.     public function getScheduledCollections(object $document): array
  2277.     {
  2278.         $oid spl_object_hash($document);
  2279.         return $this->hasScheduledCollections[$oid] ?? [];
  2280.     }
  2281.     /**
  2282.      * Checks whether the document is related to a PersistentCollection
  2283.      * scheduled for update or deletion.
  2284.      *
  2285.      * @internal
  2286.      */
  2287.     public function hasScheduledCollections(object $document): bool
  2288.     {
  2289.         return isset($this->hasScheduledCollections[spl_object_hash($document)]);
  2290.     }
  2291.     /**
  2292.      * Marks the PersistentCollection's top-level owner as having a relation to
  2293.      * a collection scheduled for update or deletion.
  2294.      *
  2295.      * If the owner is not scheduled for any lifecycle action, it will be
  2296.      * scheduled for update to ensure that versioning takes place if necessary.
  2297.      *
  2298.      * If the collection is nested within atomic collection, it is immediately
  2299.      * unscheduled and atomic one is scheduled for update instead. This makes
  2300.      * calculating update data way easier.
  2301.      *
  2302.      * @psalm-param PersistentCollectionInterface<array-key, object> $coll
  2303.      */
  2304.     private function scheduleCollectionOwner(PersistentCollectionInterface $coll): void
  2305.     {
  2306.         if ($coll->getOwner() === null) {
  2307.             return;
  2308.         }
  2309.         $document                                                                          $this->getOwningDocument($coll->getOwner());
  2310.         $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
  2311.         if ($document !== $coll->getOwner()) {
  2312.             $parent  $coll->getOwner();
  2313.             $mapping = [];
  2314.             while (($parentAssoc $this->getParentAssociation($parent)) !== null) {
  2315.                 [$mapping$parent] = $parentAssoc;
  2316.             }
  2317.             if (CollectionHelper::isAtomic($mapping['strategy'])) {
  2318.                 $class            $this->dm->getClassMetadata(get_class($document));
  2319.                 $atomicCollection $class->getFieldValue($document$mapping['fieldName']);
  2320.                 $this->scheduleCollectionUpdate($atomicCollection);
  2321.                 $this->unscheduleCollectionDeletion($coll);
  2322.                 $this->unscheduleCollectionUpdate($coll);
  2323.             }
  2324.         }
  2325.         if ($this->isDocumentScheduled($document)) {
  2326.             return;
  2327.         }
  2328.         $this->scheduleForUpdate($document);
  2329.     }
  2330.     /**
  2331.      * Get the top-most owning document of a given document
  2332.      *
  2333.      * If a top-level document is provided, that same document will be returned.
  2334.      * For an embedded document, we will walk through parent associations until
  2335.      * we find a top-level document.
  2336.      *
  2337.      * @throws UnexpectedValueException When a top-level document could not be found.
  2338.      */
  2339.     public function getOwningDocument(object $document): object
  2340.     {
  2341.         $class $this->dm->getClassMetadata(get_class($document));
  2342.         while ($class->isEmbeddedDocument) {
  2343.             $parentAssociation $this->getParentAssociation($document);
  2344.             if (! $parentAssociation) {
  2345.                 throw new UnexpectedValueException('Could not determine parent association for ' get_class($document));
  2346.             }
  2347.             [, $document] = $parentAssociation;
  2348.             $class        $this->dm->getClassMetadata(get_class($document));
  2349.         }
  2350.         return $document;
  2351.     }
  2352.     /**
  2353.      * Gets the class name for an association (embed or reference) with respect
  2354.      * to any discriminator value.
  2355.      *
  2356.      * @internal
  2357.      *
  2358.      * @param FieldMapping              $mapping
  2359.      * @param array<string, mixed>|null $data
  2360.      *
  2361.      * @psalm-return class-string
  2362.      */
  2363.     public function getClassNameForAssociation(array $mapping$data): string
  2364.     {
  2365.         $discriminatorField $mapping['discriminatorField'] ?? null;
  2366.         $discriminatorValue null;
  2367.         if (isset($discriminatorField$data[$discriminatorField])) {
  2368.             $discriminatorValue $data[$discriminatorField];
  2369.         } elseif (isset($mapping['defaultDiscriminatorValue'])) {
  2370.             $discriminatorValue $mapping['defaultDiscriminatorValue'];
  2371.         }
  2372.         if ($discriminatorValue !== null) {
  2373.             return $mapping['discriminatorMap'][$discriminatorValue]
  2374.                 ?? (string) $discriminatorValue;
  2375.         }
  2376.         $class $this->dm->getClassMetadata($mapping['targetDocument']);
  2377.         if (isset($class->discriminatorField$data[$class->discriminatorField])) {
  2378.             $discriminatorValue $data[$class->discriminatorField];
  2379.         } elseif ($class->defaultDiscriminatorValue !== null) {
  2380.             $discriminatorValue $class->defaultDiscriminatorValue;
  2381.         }
  2382.         if ($discriminatorValue !== null) {
  2383.             return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
  2384.         }
  2385.         return $mapping['targetDocument'];
  2386.     }
  2387.     /**
  2388.      * Creates a document. Used for reconstitution of documents during hydration.
  2389.      *
  2390.      * @psalm-param class-string<T> $className
  2391.      * @psalm-param array<string, mixed> $data
  2392.      * @psalm-param T|null $document
  2393.      * @psalm-param Hints $hints
  2394.      *
  2395.      * @psalm-return T
  2396.      *
  2397.      * @template T of object
  2398.      */
  2399.     public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document null): object
  2400.     {
  2401.         $class $this->dm->getClassMetadata($className);
  2402.         // @TODO figure out how to remove this
  2403.         $discriminatorValue null;
  2404.         if (isset($class->discriminatorField$data[$class->discriminatorField])) {
  2405.             $discriminatorValue $data[$class->discriminatorField];
  2406.         } elseif (isset($class->defaultDiscriminatorValue)) {
  2407.             $discriminatorValue $class->defaultDiscriminatorValue;
  2408.         }
  2409.         if ($discriminatorValue !== null) {
  2410.             /** @psalm-var class-string<T> $className */
  2411.             $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
  2412.             $class $this->dm->getClassMetadata($className);
  2413.             unset($data[$class->discriminatorField]);
  2414.         }
  2415.         if (! empty($hints[Query::HINT_READ_ONLY])) {
  2416.             /** @psalm-var T $document */
  2417.             $document $class->newInstance();
  2418.             $this->hydratorFactory->hydrate($document$data$hints);
  2419.             return $document;
  2420.         }
  2421.         $isManagedObject false;
  2422.         $serializedId    null;
  2423.         $id              null;
  2424.         if (! $class->isQueryResultDocument) {
  2425.             $id              $class->getDatabaseIdentifierValue($data['_id']);
  2426.             $serializedId    serialize($id);
  2427.             $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
  2428.         }
  2429.         $oid null;
  2430.         if ($isManagedObject) {
  2431.             /** @psalm-var T $document */
  2432.             $document $this->identityMap[$class->name][$serializedId];
  2433.             $oid      spl_object_hash($document);
  2434.             if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  2435.                 $document->setProxyInitializer(null);
  2436.                 $overrideLocalValues true;
  2437.                 if ($document instanceof NotifyPropertyChanged) {
  2438.                     $document->addPropertyChangedListener($this);
  2439.                 }
  2440.             } else {
  2441.                 $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
  2442.             }
  2443.             if ($overrideLocalValues) {
  2444.                 $data                             $this->hydratorFactory->hydrate($document$data$hints);
  2445.                 $this->originalDocumentData[$oid] = $data;
  2446.             }
  2447.         } else {
  2448.             if ($document === null) {
  2449.                 /** @psalm-var T $document */
  2450.                 $document $class->newInstance();
  2451.             }
  2452.             if (! $class->isQueryResultDocument) {
  2453.                 $this->registerManaged($document$id$data);
  2454.                 $oid                                            spl_object_hash($document);
  2455.                 $this->documentStates[$oid]                     = self::STATE_MANAGED;
  2456.                 $this->identityMap[$class->name][$serializedId] = $document;
  2457.             }
  2458.             $data $this->hydratorFactory->hydrate($document$data$hints);
  2459.             if (! $class->isQueryResultDocument && ! $class->isView()) {
  2460.                 $this->originalDocumentData[$oid] = $data;
  2461.             }
  2462.         }
  2463.         return $document;
  2464.     }
  2465.     /**
  2466.      * Initializes (loads) an uninitialized persistent collection of a document.
  2467.      *
  2468.      * @internal
  2469.      *
  2470.      * @psalm-param PersistentCollectionInterface<array-key, object> $collection
  2471.      */
  2472.     public function loadCollection(PersistentCollectionInterface $collection): void
  2473.     {
  2474.         if ($collection->getOwner() === null) {
  2475.             throw PersistentCollectionException::ownerRequiredToLoadCollection();
  2476.         }
  2477.         $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
  2478.         $this->lifecycleEventManager->postCollectionLoad($collection);
  2479.     }
  2480.     /**
  2481.      * Gets the identity map of the UnitOfWork.
  2482.      *
  2483.      * @internal
  2484.      *
  2485.      * @psalm-return array<class-string, array<string, object>>
  2486.      */
  2487.     public function getIdentityMap(): array
  2488.     {
  2489.         return $this->identityMap;
  2490.     }
  2491.     /**
  2492.      * Gets the original data of a document. The original data is the data that was
  2493.      * present at the time the document was reconstituted from the database.
  2494.      *
  2495.      * @return array<string, mixed>
  2496.      */
  2497.     public function getOriginalDocumentData(object $document): array
  2498.     {
  2499.         $oid spl_object_hash($document);
  2500.         return $this->originalDocumentData[$oid] ?? [];
  2501.     }
  2502.     /**
  2503.      * @internal
  2504.      *
  2505.      * @param array<string, mixed> $data
  2506.      */
  2507.     public function setOriginalDocumentData(object $document, array $data): void
  2508.     {
  2509.         $oid                              spl_object_hash($document);
  2510.         $this->originalDocumentData[$oid] = $data;
  2511.         unset($this->documentChangeSets[$oid]);
  2512.     }
  2513.     /**
  2514.      * Sets a property value of the original data array of a document.
  2515.      *
  2516.      * @internal
  2517.      *
  2518.      * @param mixed $value
  2519.      */
  2520.     public function setOriginalDocumentProperty(string $oidstring $property$value): void
  2521.     {
  2522.         $this->originalDocumentData[$oid][$property] = $value;
  2523.     }
  2524.     /**
  2525.      * Gets the identifier of a document.
  2526.      *
  2527.      * @return mixed The identifier value
  2528.      */
  2529.     public function getDocumentIdentifier(object $document)
  2530.     {
  2531.         return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
  2532.     }
  2533.     /**
  2534.      * Checks whether the UnitOfWork has any pending insertions.
  2535.      *
  2536.      * @internal
  2537.      *
  2538.      * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
  2539.      */
  2540.     public function hasPendingInsertions(): bool
  2541.     {
  2542.         return ! empty($this->documentInsertions);
  2543.     }
  2544.     /**
  2545.      * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
  2546.      * number of documents in the identity map.
  2547.      *
  2548.      * @internal
  2549.      */
  2550.     public function size(): int
  2551.     {
  2552.         $count 0;
  2553.         foreach ($this->identityMap as $documentSet) {
  2554.             $count += count($documentSet);
  2555.         }
  2556.         return $count;
  2557.     }
  2558.     /**
  2559.      * Registers a document as managed.
  2560.      *
  2561.      * TODO: This method assumes that $id is a valid PHP identifier for the
  2562.      * document class. If the class expects its database identifier to be an
  2563.      * ObjectId, and an incompatible $id is registered (e.g. an integer), the
  2564.      * document identifiers map will become inconsistent with the identity map.
  2565.      * In the future, we may want to round-trip $id through a PHP and database
  2566.      * conversion and throw an exception if it's inconsistent.
  2567.      *
  2568.      * @internal
  2569.      *
  2570.      * @param mixed                $id   The identifier values.
  2571.      * @param array<string, mixed> $data
  2572.      */
  2573.     public function registerManaged(object $document$id, array $data): void
  2574.     {
  2575.         $oid   spl_object_hash($document);
  2576.         $class $this->dm->getClassMetadata(get_class($document));
  2577.         if (! $class->identifier || $id === null) {
  2578.             $this->documentIdentifiers[$oid] = $oid;
  2579.         } else {
  2580.             $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
  2581.         }
  2582.         $this->documentStates[$oid]       = self::STATE_MANAGED;
  2583.         $this->originalDocumentData[$oid] = $data;
  2584.         $this->addToIdentityMap($document);
  2585.     }
  2586.     /**
  2587.      * Clears the property changeset of the document with the given OID.
  2588.      *
  2589.      * @internal
  2590.      */
  2591.     public function clearDocumentChangeSet(string $oid): void
  2592.     {
  2593.         $this->documentChangeSets[$oid] = [];
  2594.     }
  2595.     /* PropertyChangedListener implementation */
  2596.     /**
  2597.      * Notifies this UnitOfWork of a property change in a document.
  2598.      *
  2599.      * @param object $sender       The document that owns the property.
  2600.      * @param string $propertyName The name of the property that changed.
  2601.      * @param mixed  $oldValue     The old value of the property.
  2602.      * @param mixed  $newValue     The new value of the property.
  2603.      */
  2604.     public function propertyChanged($sender$propertyName$oldValue$newValue)
  2605.     {
  2606.         $oid   spl_object_hash($sender);
  2607.         $class $this->dm->getClassMetadata(get_class($sender));
  2608.         if (! isset($class->fieldMappings[$propertyName])) {
  2609.             return; // ignore non-persistent fields
  2610.         }
  2611.         // Update changeset and mark document for synchronization
  2612.         $this->documentChangeSets[$oid][$propertyName] = [$oldValue$newValue];
  2613.         if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
  2614.             return;
  2615.         }
  2616.         $this->scheduleForSynchronization($sender);
  2617.     }
  2618.     /**
  2619.      * Gets the currently scheduled document insertions in this UnitOfWork.
  2620.      *
  2621.      * @psalm-return array<string, object>
  2622.      */
  2623.     public function getScheduledDocumentInsertions(): array
  2624.     {
  2625.         return $this->documentInsertions;
  2626.     }
  2627.     /**
  2628.      * Gets the currently scheduled document upserts in this UnitOfWork.
  2629.      *
  2630.      * @psalm-return array<string, object>
  2631.      */
  2632.     public function getScheduledDocumentUpserts(): array
  2633.     {
  2634.         return $this->documentUpserts;
  2635.     }
  2636.     /**
  2637.      * Gets the currently scheduled document updates in this UnitOfWork.
  2638.      *
  2639.      * @psalm-return array<string, object>
  2640.      */
  2641.     public function getScheduledDocumentUpdates(): array
  2642.     {
  2643.         return $this->documentUpdates;
  2644.     }
  2645.     /**
  2646.      * Gets the currently scheduled document deletions in this UnitOfWork.
  2647.      *
  2648.      * @psalm-return array<string, object>
  2649.      */
  2650.     public function getScheduledDocumentDeletions(): array
  2651.     {
  2652.         return $this->documentDeletions;
  2653.     }
  2654.     /**
  2655.      * Get the currently scheduled complete collection deletions
  2656.      *
  2657.      * @internal
  2658.      *
  2659.      * @psalm-return array<string, PersistentCollectionInterface<array-key, object>>
  2660.      */
  2661.     public function getScheduledCollectionDeletions(): array
  2662.     {
  2663.         return $this->collectionDeletions;
  2664.     }
  2665.     /**
  2666.      * Gets the currently scheduled collection inserts, updates and deletes.
  2667.      *
  2668.      * @internal
  2669.      *
  2670.      * @psalm-return array<string, PersistentCollectionInterface<array-key, object>>
  2671.      */
  2672.     public function getScheduledCollectionUpdates(): array
  2673.     {
  2674.         return $this->collectionUpdates;
  2675.     }
  2676.     /**
  2677.      * Helper method to initialize a lazy loading proxy or persistent collection.
  2678.      *
  2679.      * @internal
  2680.      */
  2681.     public function initializeObject(object $obj): void
  2682.     {
  2683.         if ($obj instanceof GhostObjectInterface) {
  2684.             $obj->initializeProxy();
  2685.         } elseif ($obj instanceof PersistentCollectionInterface) {
  2686.             $obj->initialize();
  2687.         }
  2688.     }
  2689.     private function objToStr(object $obj): string
  2690.     {
  2691.         return method_exists($obj'__toString') ? (string) $obj get_class($obj) . '@' spl_object_hash($obj);
  2692.     }
  2693. }