vendor\doctrine\orm\src\PersistentCollection.php line 43

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM;
  4. use Doctrine\Common\Collections\AbstractLazyCollection;
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\Common\Collections\Collection;
  7. use Doctrine\Common\Collections\Criteria;
  8. use Doctrine\Common\Collections\Selectable;
  9. use Doctrine\ORM\Internal\CriteriaOrderings;
  10. use Doctrine\ORM\Mapping\ClassMetadata;
  11. use ReturnTypeWillChange;
  12. use RuntimeException;
  13. use UnexpectedValueException;
  14. use function array_combine;
  15. use function array_diff_key;
  16. use function array_map;
  17. use function array_values;
  18. use function array_walk;
  19. use function assert;
  20. use function get_class;
  21. use function is_object;
  22. use function spl_object_id;
  23. /**
  24.  * A PersistentCollection represents a collection of elements that have persistent state.
  25.  *
  26.  * Collections of entities represent only the associations (links) to those entities.
  27.  * That means, if the collection is part of a many-many mapping and you remove
  28.  * entities from the collection, only the links in the relation table are removed (on flush).
  29.  * Similarly, if you remove entities from a collection that is part of a one-many
  30.  * mapping this will only result in the nulling out of the foreign keys on flush.
  31.  *
  32.  * @psalm-template TKey of array-key
  33.  * @psalm-template T
  34.  * @template-extends AbstractLazyCollection<TKey,T>
  35.  * @template-implements Selectable<TKey,T>
  36.  * @psalm-import-type AssociationMapping from ClassMetadata
  37.  */
  38. final class PersistentCollection extends AbstractLazyCollection implements Selectable
  39. {
  40.     use CriteriaOrderings;
  41.     /**
  42.      * A snapshot of the collection at the moment it was fetched from the database.
  43.      * This is used to create a diff of the collection at commit time.
  44.      *
  45.      * @psalm-var array<string|int, mixed>
  46.      */
  47.     private $snapshot = [];
  48.     /**
  49.      * The entity that owns this collection.
  50.      *
  51.      * @var object|null
  52.      */
  53.     private $owner;
  54.     /**
  55.      * The association mapping the collection belongs to.
  56.      * This is currently either a OneToManyMapping or a ManyToManyMapping.
  57.      *
  58.      * @psalm-var AssociationMapping|null
  59.      */
  60.     private $association;
  61.     /**
  62.      * The EntityManager that manages the persistence of the collection.
  63.      *
  64.      * @var EntityManagerInterface|null
  65.      */
  66.     private $em;
  67.     /**
  68.      * The name of the field on the target entities that points to the owner
  69.      * of the collection. This is only set if the association is bi-directional.
  70.      *
  71.      * @var string|null
  72.      */
  73.     private $backRefFieldName;
  74.     /**
  75.      * The class descriptor of the collection's entity type.
  76.      *
  77.      * @var ClassMetadata|null
  78.      */
  79.     private $typeClass;
  80.     /**
  81.      * Whether the collection is dirty and needs to be synchronized with the database
  82.      * when the UnitOfWork that manages its persistent state commits.
  83.      *
  84.      * @var bool
  85.      */
  86.     private $isDirty false;
  87.     /**
  88.      * Creates a new persistent collection.
  89.      *
  90.      * @param EntityManagerInterface $em    The EntityManager the collection will be associated with.
  91.      * @param ClassMetadata          $class The class descriptor of the entity type of this collection.
  92.      * @psalm-param Collection<TKey, T>&Selectable<TKey, T> $collection The collection elements.
  93.      */
  94.     public function __construct(EntityManagerInterface $em$classCollection $collection)
  95.     {
  96.         $this->collection  $collection;
  97.         $this->em          $em;
  98.         $this->typeClass   $class;
  99.         $this->initialized true;
  100.     }
  101.     /**
  102.      * INTERNAL:
  103.      * Sets the collection's owning entity together with the AssociationMapping that
  104.      * describes the association between the owner and the elements of the collection.
  105.      *
  106.      * @param object $entity
  107.      * @psalm-param AssociationMapping $assoc
  108.      */
  109.     public function setOwner($entity, array $assoc): void
  110.     {
  111.         $this->owner            $entity;
  112.         $this->association      $assoc;
  113.         $this->backRefFieldName $assoc['inversedBy'] ?: $assoc['mappedBy'];
  114.     }
  115.     /**
  116.      * INTERNAL:
  117.      * Gets the collection owner.
  118.      *
  119.      * @return object|null
  120.      */
  121.     public function getOwner()
  122.     {
  123.         return $this->owner;
  124.     }
  125.     /** @return Mapping\ClassMetadata */
  126.     public function getTypeClass(): Mapping\ClassMetadataInfo
  127.     {
  128.         assert($this->typeClass !== null);
  129.         return $this->typeClass;
  130.     }
  131.     private function getUnitOfWork(): UnitOfWork
  132.     {
  133.         assert($this->em !== null);
  134.         return $this->em->getUnitOfWork();
  135.     }
  136.     /**
  137.      * INTERNAL:
  138.      * Adds an element to a collection during hydration. This will automatically
  139.      * complete bidirectional associations in the case of a one-to-many association.
  140.      *
  141.      * @param mixed $element The element to add.
  142.      */
  143.     public function hydrateAdd($element): void
  144.     {
  145.         $this->unwrap()->add($element);
  146.         // If _backRefFieldName is set and its a one-to-many association,
  147.         // we need to set the back reference.
  148.         if ($this->backRefFieldName && $this->getMapping()['type'] === ClassMetadata::ONE_TO_MANY) {
  149.             assert($this->typeClass !== null);
  150.             // Set back reference to owner
  151.             $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
  152.                 $element,
  153.                 $this->owner
  154.             );
  155.             $this->getUnitOfWork()->setOriginalEntityProperty(
  156.                 spl_object_id($element),
  157.                 $this->backRefFieldName,
  158.                 $this->owner
  159.             );
  160.         }
  161.     }
  162.     /**
  163.      * INTERNAL:
  164.      * Sets a keyed element in the collection during hydration.
  165.      *
  166.      * @param mixed $key     The key to set.
  167.      * @param mixed $element The element to set.
  168.      */
  169.     public function hydrateSet($key$element): void
  170.     {
  171.         $this->unwrap()->set($key$element);
  172.         // If _backRefFieldName is set, then the association is bidirectional
  173.         // and we need to set the back reference.
  174.         if ($this->backRefFieldName && $this->getMapping()['type'] === ClassMetadata::ONE_TO_MANY) {
  175.             assert($this->typeClass !== null);
  176.             // Set back reference to owner
  177.             $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
  178.                 $element,
  179.                 $this->owner
  180.             );
  181.         }
  182.     }
  183.     /**
  184.      * Initializes the collection by loading its contents from the database
  185.      * if the collection is not yet initialized.
  186.      */
  187.     public function initialize(): void
  188.     {
  189.         if ($this->initialized || ! $this->association) {
  190.             return;
  191.         }
  192.         $this->doInitialize();
  193.         $this->initialized true;
  194.     }
  195.     /**
  196.      * INTERNAL:
  197.      * Tells this collection to take a snapshot of its current state.
  198.      */
  199.     public function takeSnapshot(): void
  200.     {
  201.         $this->snapshot $this->unwrap()->toArray();
  202.         $this->isDirty  false;
  203.     }
  204.     /**
  205.      * INTERNAL:
  206.      * Returns the last snapshot of the elements in the collection.
  207.      *
  208.      * @psalm-return array<string|int, mixed> The last snapshot of the elements.
  209.      */
  210.     public function getSnapshot(): array
  211.     {
  212.         return $this->snapshot;
  213.     }
  214.     /**
  215.      * INTERNAL:
  216.      * getDeleteDiff
  217.      *
  218.      * @return mixed[]
  219.      */
  220.     public function getDeleteDiff(): array
  221.     {
  222.         $collectionItems $this->unwrap()->toArray();
  223.         return array_values(array_diff_key(
  224.             array_combine(array_map('spl_object_id'$this->snapshot), $this->snapshot),
  225.             array_combine(array_map('spl_object_id'$collectionItems), $collectionItems)
  226.         ));
  227.     }
  228.     /**
  229.      * INTERNAL:
  230.      * getInsertDiff
  231.      *
  232.      * @return mixed[]
  233.      */
  234.     public function getInsertDiff(): array
  235.     {
  236.         $collectionItems $this->unwrap()->toArray();
  237.         return array_values(array_diff_key(
  238.             array_combine(array_map('spl_object_id'$collectionItems), $collectionItems),
  239.             array_combine(array_map('spl_object_id'$this->snapshot), $this->snapshot)
  240.         ));
  241.     }
  242.     /**
  243.      * INTERNAL: Gets the association mapping of the collection.
  244.      *
  245.      * @psalm-return AssociationMapping
  246.      */
  247.     public function getMapping(): array
  248.     {
  249.         if ($this->association === null) {
  250.             throw new UnexpectedValueException('The underlying association mapping is null although it should not be');
  251.         }
  252.         return $this->association;
  253.     }
  254.     /**
  255.      * Marks this collection as changed/dirty.
  256.      */
  257.     private function changed(): void
  258.     {
  259.         if ($this->isDirty) {
  260.             return;
  261.         }
  262.         $this->isDirty true;
  263.         if (
  264.             $this->association !== null &&
  265.             $this->getMapping()['isOwningSide'] &&
  266.             $this->getMapping()['type'] === ClassMetadata::MANY_TO_MANY &&
  267.             $this->owner &&
  268.             $this->em !== null &&
  269.             $this->em->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()
  270.         ) {
  271.             $this->getUnitOfWork()->scheduleForDirtyCheck($this->owner);
  272.         }
  273.     }
  274.     /**
  275.      * Gets a boolean flag indicating whether this collection is dirty which means
  276.      * its state needs to be synchronized with the database.
  277.      *
  278.      * @return bool TRUE if the collection is dirty, FALSE otherwise.
  279.      */
  280.     public function isDirty(): bool
  281.     {
  282.         return $this->isDirty;
  283.     }
  284.     /**
  285.      * Sets a boolean flag, indicating whether this collection is dirty.
  286.      *
  287.      * @param bool $dirty Whether the collection should be marked dirty or not.
  288.      */
  289.     public function setDirty($dirty): void
  290.     {
  291.         $this->isDirty $dirty;
  292.     }
  293.     /**
  294.      * Sets the initialized flag of the collection, forcing it into that state.
  295.      *
  296.      * @param bool $bool
  297.      */
  298.     public function setInitialized($bool): void
  299.     {
  300.         $this->initialized $bool;
  301.     }
  302.     /**
  303.      * {@inheritDoc}
  304.      */
  305.     public function remove($key)
  306.     {
  307.         // TODO: If the keys are persistent as well (not yet implemented)
  308.         //       and the collection is not initialized and orphanRemoval is
  309.         //       not used we can issue a straight SQL delete/update on the
  310.         //       association (table). Without initializing the collection.
  311.         $removed parent::remove($key);
  312.         if (! $removed) {
  313.             return $removed;
  314.         }
  315.         $this->changed();
  316.         if (
  317.             $this->association !== null &&
  318.             $this->getMapping()['type'] & ClassMetadata::TO_MANY &&
  319.             $this->owner &&
  320.             $this->getMapping()['orphanRemoval']
  321.         ) {
  322.             $this->getUnitOfWork()->scheduleOrphanRemoval($removed);
  323.         }
  324.         return $removed;
  325.     }
  326.     /**
  327.      * {@inheritDoc}
  328.      */
  329.     public function removeElement($element): bool
  330.     {
  331.         $removed parent::removeElement($element);
  332.         if (! $removed) {
  333.             return $removed;
  334.         }
  335.         $this->changed();
  336.         if (
  337.             $this->association !== null &&
  338.             $this->getMapping()['type'] & ClassMetadata::TO_MANY &&
  339.             $this->owner &&
  340.             $this->getMapping()['orphanRemoval']
  341.         ) {
  342.             $this->getUnitOfWork()->scheduleOrphanRemoval($element);
  343.         }
  344.         return $removed;
  345.     }
  346.     /**
  347.      * {@inheritDoc}
  348.      */
  349.     public function containsKey($key): bool
  350.     {
  351.         if (
  352.             ! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY
  353.             && isset($this->getMapping()['indexBy'])
  354.         ) {
  355.             $persister $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
  356.             return $this->unwrap()->containsKey($key) || $persister->containsKey($this$key);
  357.         }
  358.         return parent::containsKey($key);
  359.     }
  360.     /**
  361.      * {@inheritDoc}
  362.      *
  363.      * @template TMaybeContained
  364.      */
  365.     public function contains($element): bool
  366.     {
  367.         if (! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  368.             $persister $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
  369.             return $this->unwrap()->contains($element) || $persister->contains($this$element);
  370.         }
  371.         return parent::contains($element);
  372.     }
  373.     /**
  374.      * {@inheritDoc}
  375.      */
  376.     public function get($key)
  377.     {
  378.         if (
  379.             ! $this->initialized
  380.             && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY
  381.             && isset($this->getMapping()['indexBy'])
  382.         ) {
  383.             assert($this->em !== null);
  384.             assert($this->typeClass !== null);
  385.             if (! $this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()['indexBy'])) {
  386.                 return $this->em->find($this->typeClass->name$key);
  387.             }
  388.             return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this$key);
  389.         }
  390.         return parent::get($key);
  391.     }
  392.     public function count(): int
  393.     {
  394.         if (! $this->initialized && $this->association !== null && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  395.             $persister $this->getUnitOfWork()->getCollectionPersister($this->association);
  396.             return $persister->count($this) + ($this->isDirty $this->unwrap()->count() : 0);
  397.         }
  398.         return parent::count();
  399.     }
  400.     /**
  401.      * {@inheritDoc}
  402.      */
  403.     public function set($key$value): void
  404.     {
  405.         parent::set($key$value);
  406.         $this->changed();
  407.         if (is_object($value) && $this->em) {
  408.             $this->getUnitOfWork()->cancelOrphanRemoval($value);
  409.         }
  410.     }
  411.     /**
  412.      * {@inheritDoc}
  413.      */
  414.     public function add($value): bool
  415.     {
  416.         $this->unwrap()->add($value);
  417.         $this->changed();
  418.         if (is_object($value) && $this->em) {
  419.             $this->getUnitOfWork()->cancelOrphanRemoval($value);
  420.         }
  421.         return true;
  422.     }
  423.     /* ArrayAccess implementation */
  424.     /**
  425.      * {@inheritDoc}
  426.      */
  427.     public function offsetExists($offset): bool
  428.     {
  429.         return $this->containsKey($offset);
  430.     }
  431.     /**
  432.      * {@inheritDoc}
  433.      */
  434.     #[ReturnTypeWillChange]
  435.     public function offsetGet($offset)
  436.     {
  437.         return $this->get($offset);
  438.     }
  439.     /**
  440.      * {@inheritDoc}
  441.      */
  442.     public function offsetSet($offset$value): void
  443.     {
  444.         if (! isset($offset)) {
  445.             $this->add($value);
  446.             return;
  447.         }
  448.         $this->set($offset$value);
  449.     }
  450.     /**
  451.      * {@inheritDoc}
  452.      *
  453.      * @return object|null
  454.      */
  455.     #[ReturnTypeWillChange]
  456.     public function offsetUnset($offset)
  457.     {
  458.         return $this->remove($offset);
  459.     }
  460.     public function isEmpty(): bool
  461.     {
  462.         return $this->unwrap()->isEmpty() && $this->count() === 0;
  463.     }
  464.     public function clear(): void
  465.     {
  466.         if ($this->initialized && $this->isEmpty()) {
  467.             $this->unwrap()->clear();
  468.             return;
  469.         }
  470.         $uow         $this->getUnitOfWork();
  471.         $association $this->getMapping();
  472.         if (
  473.             $association['type'] & ClassMetadata::TO_MANY &&
  474.             $association['orphanRemoval'] &&
  475.             $this->owner
  476.         ) {
  477.             // we need to initialize here, as orphan removal acts like implicit cascadeRemove,
  478.             // hence for event listeners we need the objects in memory.
  479.             $this->initialize();
  480.             foreach ($this->unwrap() as $element) {
  481.                 $uow->scheduleOrphanRemoval($element);
  482.             }
  483.         }
  484.         $this->unwrap()->clear();
  485.         $this->initialized true// direct call, {@link initialize()} is too expensive
  486.         if ($association['isOwningSide'] && $this->owner) {
  487.             $this->changed();
  488.             $uow->scheduleCollectionDeletion($this);
  489.             $this->takeSnapshot();
  490.         }
  491.     }
  492.     /**
  493.      * Called by PHP when this collection is serialized. Ensures that only the
  494.      * elements are properly serialized.
  495.      *
  496.      * Internal note: Tried to implement Serializable first but that did not work well
  497.      *                with circular references. This solution seems simpler and works well.
  498.      *
  499.      * @return string[]
  500.      * @psalm-return array{0: string, 1: string}
  501.      */
  502.     public function __sleep(): array
  503.     {
  504.         return ['collection''initialized'];
  505.     }
  506.     /**
  507.      * Extracts a slice of $length elements starting at position $offset from the Collection.
  508.      *
  509.      * If $length is null it returns all elements from $offset to the end of the Collection.
  510.      * Keys have to be preserved by this method. Calling this method will only return the
  511.      * selected slice and NOT change the elements contained in the collection slice is called on.
  512.      *
  513.      * @param int      $offset
  514.      * @param int|null $length
  515.      *
  516.      * @return mixed[]
  517.      * @psalm-return array<TKey,T>
  518.      */
  519.     public function slice($offset$length null): array
  520.     {
  521.         if (! $this->initialized && ! $this->isDirty && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  522.             $persister $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
  523.             return $persister->slice($this$offset$length);
  524.         }
  525.         return parent::slice($offset$length);
  526.     }
  527.     /**
  528.      * Cleans up internal state of cloned persistent collection.
  529.      *
  530.      * The following problems have to be prevented:
  531.      * 1. Added entities are added to old PC
  532.      * 2. New collection is not dirty, if reused on other entity nothing
  533.      * changes.
  534.      * 3. Snapshot leads to invalid diffs being generated.
  535.      * 4. Lazy loading grabs entities from old owner object.
  536.      * 5. New collection is connected to old owner and leads to duplicate keys.
  537.      */
  538.     public function __clone()
  539.     {
  540.         if (is_object($this->collection)) {
  541.             $this->collection = clone $this->collection;
  542.         }
  543.         $this->initialize();
  544.         $this->owner    null;
  545.         $this->snapshot = [];
  546.         $this->changed();
  547.     }
  548.     /**
  549.      * Selects all elements from a selectable that match the expression and
  550.      * return a new collection containing these elements.
  551.      *
  552.      * @psalm-return Collection<TKey, T>
  553.      *
  554.      * @throws RuntimeException
  555.      */
  556.     public function matching(Criteria $criteria): Collection
  557.     {
  558.         if ($this->isDirty) {
  559.             $this->initialize();
  560.         }
  561.         if ($this->initialized) {
  562.             return $this->unwrap()->matching($criteria);
  563.         }
  564.         $association $this->getMapping();
  565.         if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
  566.             $persister $this->getUnitOfWork()->getCollectionPersister($association);
  567.             return new ArrayCollection($persister->loadCriteria($this$criteria));
  568.         }
  569.         $builder         Criteria::expr();
  570.         $ownerExpression $builder->eq($this->backRefFieldName$this->owner);
  571.         $expression      $criteria->getWhereExpression();
  572.         $expression      $expression $builder->andX($expression$ownerExpression) : $ownerExpression;
  573.         $criteria = clone $criteria;
  574.         $criteria->where($expression);
  575.         $criteria->orderBy(self::mapToOrderEnumIfAvailable(
  576.             self::getCriteriaOrderings($criteria) ?: $association['orderBy'] ?? []
  577.         ));
  578.         $persister $this->getUnitOfWork()->getEntityPersister($association['targetEntity']);
  579.         return $association['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY
  580.             ? new LazyCriteriaCollection($persister$criteria)
  581.             : new ArrayCollection($persister->loadCriteria($criteria));
  582.     }
  583.     /**
  584.      * Retrieves the wrapped Collection instance.
  585.      *
  586.      * @return Collection<TKey, T>&Selectable<TKey, T>
  587.      */
  588.     public function unwrap(): Collection
  589.     {
  590.         assert($this->collection instanceof Collection);
  591.         assert($this->collection instanceof Selectable);
  592.         return $this->collection;
  593.     }
  594.     protected function doInitialize(): void
  595.     {
  596.         // Has NEW objects added through add(). Remember them.
  597.         $newlyAddedDirtyObjects = [];
  598.         if ($this->isDirty) {
  599.             $newlyAddedDirtyObjects $this->unwrap()->toArray();
  600.         }
  601.         $this->unwrap()->clear();
  602.         $this->getUnitOfWork()->loadCollection($this);
  603.         $this->takeSnapshot();
  604.         if ($newlyAddedDirtyObjects) {
  605.             $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects);
  606.         }
  607.     }
  608.     /**
  609.      * @param object[] $newObjects
  610.      *
  611.      * Note: the only reason why this entire looping/complexity is performed via `spl_object_id`
  612.      *       is because we want to prevent using `array_udiff()`, which is likely to cause very
  613.      *       high overhead (complexity of O(n^2)). `array_diff_key()` performs the operation in
  614.      *       core, which is faster than using a callback for comparisons
  615.      */
  616.     private function restoreNewObjectsInDirtyCollection(array $newObjects): void
  617.     {
  618.         $loadedObjects               $this->unwrap()->toArray();
  619.         $newObjectsByOid             array_combine(array_map('spl_object_id'$newObjects), $newObjects);
  620.         $loadedObjectsByOid          array_combine(array_map('spl_object_id'$loadedObjects), $loadedObjects);
  621.         $newObjectsThatWereNotLoaded array_diff_key($newObjectsByOid$loadedObjectsByOid);
  622.         if ($newObjectsThatWereNotLoaded) {
  623.             // Reattach NEW objects added through add(), if any.
  624.             array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']);
  625.             $this->isDirty true;
  626.         }
  627.     }
  628. }