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.     public function contains($element): bool
  361.     {
  362.         if (! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  363.             $persister $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
  364.             return $this->unwrap()->contains($element) || $persister->contains($this$element);
  365.         }
  366.         return parent::contains($element);
  367.     }
  368.     /**
  369.      * {@inheritDoc}
  370.      */
  371.     public function get($key)
  372.     {
  373.         if (
  374.             ! $this->initialized
  375.             && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY
  376.             && isset($this->getMapping()['indexBy'])
  377.         ) {
  378.             assert($this->em !== null);
  379.             assert($this->typeClass !== null);
  380.             if (! $this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->getMapping()['indexBy'])) {
  381.                 return $this->em->find($this->typeClass->name$key);
  382.             }
  383.             return $this->getUnitOfWork()->getCollectionPersister($this->getMapping())->get($this$key);
  384.         }
  385.         return parent::get($key);
  386.     }
  387.     public function count(): int
  388.     {
  389.         if (! $this->initialized && $this->association !== null && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  390.             $persister $this->getUnitOfWork()->getCollectionPersister($this->association);
  391.             return $persister->count($this) + ($this->isDirty $this->unwrap()->count() : 0);
  392.         }
  393.         return parent::count();
  394.     }
  395.     /**
  396.      * {@inheritDoc}
  397.      */
  398.     public function set($key$value): void
  399.     {
  400.         parent::set($key$value);
  401.         $this->changed();
  402.         if (is_object($value) && $this->em) {
  403.             $this->getUnitOfWork()->cancelOrphanRemoval($value);
  404.         }
  405.     }
  406.     /**
  407.      * {@inheritDoc}
  408.      */
  409.     public function add($value): bool
  410.     {
  411.         $this->unwrap()->add($value);
  412.         $this->changed();
  413.         if (is_object($value) && $this->em) {
  414.             $this->getUnitOfWork()->cancelOrphanRemoval($value);
  415.         }
  416.         return true;
  417.     }
  418.     /* ArrayAccess implementation */
  419.     /**
  420.      * {@inheritDoc}
  421.      */
  422.     public function offsetExists($offset): bool
  423.     {
  424.         return $this->containsKey($offset);
  425.     }
  426.     /**
  427.      * {@inheritDoc}
  428.      */
  429.     #[ReturnTypeWillChange]
  430.     public function offsetGet($offset)
  431.     {
  432.         return $this->get($offset);
  433.     }
  434.     /**
  435.      * {@inheritDoc}
  436.      */
  437.     public function offsetSet($offset$value): void
  438.     {
  439.         if (! isset($offset)) {
  440.             $this->add($value);
  441.             return;
  442.         }
  443.         $this->set($offset$value);
  444.     }
  445.     /**
  446.      * {@inheritDoc}
  447.      *
  448.      * @return object|null
  449.      */
  450.     #[ReturnTypeWillChange]
  451.     public function offsetUnset($offset)
  452.     {
  453.         return $this->remove($offset);
  454.     }
  455.     public function isEmpty(): bool
  456.     {
  457.         return $this->unwrap()->isEmpty() && $this->count() === 0;
  458.     }
  459.     public function clear(): void
  460.     {
  461.         if ($this->initialized && $this->isEmpty()) {
  462.             $this->unwrap()->clear();
  463.             return;
  464.         }
  465.         $uow         $this->getUnitOfWork();
  466.         $association $this->getMapping();
  467.         if (
  468.             $association['type'] & ClassMetadata::TO_MANY &&
  469.             $association['orphanRemoval'] &&
  470.             $this->owner
  471.         ) {
  472.             // we need to initialize here, as orphan removal acts like implicit cascadeRemove,
  473.             // hence for event listeners we need the objects in memory.
  474.             $this->initialize();
  475.             foreach ($this->unwrap() as $element) {
  476.                 $uow->scheduleOrphanRemoval($element);
  477.             }
  478.         }
  479.         $this->unwrap()->clear();
  480.         $this->initialized true// direct call, {@link initialize()} is too expensive
  481.         if ($association['isOwningSide'] && $this->owner) {
  482.             $this->changed();
  483.             $uow->scheduleCollectionDeletion($this);
  484.             $this->takeSnapshot();
  485.         }
  486.     }
  487.     /**
  488.      * Called by PHP when this collection is serialized. Ensures that only the
  489.      * elements are properly serialized.
  490.      *
  491.      * Internal note: Tried to implement Serializable first but that did not work well
  492.      *                with circular references. This solution seems simpler and works well.
  493.      *
  494.      * @return string[]
  495.      * @psalm-return array{0: string, 1: string}
  496.      */
  497.     public function __sleep(): array
  498.     {
  499.         return ['collection''initialized'];
  500.     }
  501.     /**
  502.      * Extracts a slice of $length elements starting at position $offset from the Collection.
  503.      *
  504.      * If $length is null it returns all elements from $offset to the end of the Collection.
  505.      * Keys have to be preserved by this method. Calling this method will only return the
  506.      * selected slice and NOT change the elements contained in the collection slice is called on.
  507.      *
  508.      * @param int      $offset
  509.      * @param int|null $length
  510.      *
  511.      * @return mixed[]
  512.      * @psalm-return array<TKey,T>
  513.      */
  514.     public function slice($offset$length null): array
  515.     {
  516.         if (! $this->initialized && ! $this->isDirty && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {
  517.             $persister $this->getUnitOfWork()->getCollectionPersister($this->getMapping());
  518.             return $persister->slice($this$offset$length);
  519.         }
  520.         return parent::slice($offset$length);
  521.     }
  522.     /**
  523.      * Cleans up internal state of cloned persistent collection.
  524.      *
  525.      * The following problems have to be prevented:
  526.      * 1. Added entities are added to old PC
  527.      * 2. New collection is not dirty, if reused on other entity nothing
  528.      * changes.
  529.      * 3. Snapshot leads to invalid diffs being generated.
  530.      * 4. Lazy loading grabs entities from old owner object.
  531.      * 5. New collection is connected to old owner and leads to duplicate keys.
  532.      */
  533.     public function __clone()
  534.     {
  535.         if (is_object($this->collection)) {
  536.             $this->collection = clone $this->collection;
  537.         }
  538.         $this->initialize();
  539.         $this->owner    null;
  540.         $this->snapshot = [];
  541.         $this->changed();
  542.     }
  543.     /**
  544.      * Selects all elements from a selectable that match the expression and
  545.      * return a new collection containing these elements.
  546.      *
  547.      * @psalm-return Collection<TKey, T>
  548.      *
  549.      * @throws RuntimeException
  550.      */
  551.     public function matching(Criteria $criteria): Collection
  552.     {
  553.         if ($this->isDirty) {
  554.             $this->initialize();
  555.         }
  556.         if ($this->initialized) {
  557.             return $this->unwrap()->matching($criteria);
  558.         }
  559.         $association $this->getMapping();
  560.         if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
  561.             $persister $this->getUnitOfWork()->getCollectionPersister($association);
  562.             return new ArrayCollection($persister->loadCriteria($this$criteria));
  563.         }
  564.         $builder         Criteria::expr();
  565.         $ownerExpression $builder->eq($this->backRefFieldName$this->owner);
  566.         $expression      $criteria->getWhereExpression();
  567.         $expression      $expression $builder->andX($expression$ownerExpression) : $ownerExpression;
  568.         $criteria = clone $criteria;
  569.         $criteria->where($expression);
  570.         $criteria->orderBy(self::mapToOrderEnumIfAvailable(
  571.             self::getCriteriaOrderings($criteria) ?: $association['orderBy'] ?? []
  572.         ));
  573.         $persister $this->getUnitOfWork()->getEntityPersister($association['targetEntity']);
  574.         return $association['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY
  575.             ? new LazyCriteriaCollection($persister$criteria)
  576.             : new ArrayCollection($persister->loadCriteria($criteria));
  577.     }
  578.     /**
  579.      * Retrieves the wrapped Collection instance.
  580.      *
  581.      * @return Collection<TKey, T>&Selectable<TKey, T>
  582.      */
  583.     public function unwrap(): Collection
  584.     {
  585.         assert($this->collection instanceof Collection);
  586.         assert($this->collection instanceof Selectable);
  587.         return $this->collection;
  588.     }
  589.     protected function doInitialize(): void
  590.     {
  591.         // Has NEW objects added through add(). Remember them.
  592.         $newlyAddedDirtyObjects = [];
  593.         if ($this->isDirty) {
  594.             $newlyAddedDirtyObjects $this->unwrap()->toArray();
  595.         }
  596.         $this->unwrap()->clear();
  597.         $this->getUnitOfWork()->loadCollection($this);
  598.         $this->takeSnapshot();
  599.         if ($newlyAddedDirtyObjects) {
  600.             $this->restoreNewObjectsInDirtyCollection($newlyAddedDirtyObjects);
  601.         }
  602.     }
  603.     /**
  604.      * @param object[] $newObjects
  605.      *
  606.      * Note: the only reason why this entire looping/complexity is performed via `spl_object_id`
  607.      *       is because we want to prevent using `array_udiff()`, which is likely to cause very
  608.      *       high overhead (complexity of O(n^2)). `array_diff_key()` performs the operation in
  609.      *       core, which is faster than using a callback for comparisons
  610.      */
  611.     private function restoreNewObjectsInDirtyCollection(array $newObjects): void
  612.     {
  613.         $loadedObjects               $this->unwrap()->toArray();
  614.         $newObjectsByOid             array_combine(array_map('spl_object_id'$newObjects), $newObjects);
  615.         $loadedObjectsByOid          array_combine(array_map('spl_object_id'$loadedObjects), $loadedObjects);
  616.         $newObjectsThatWereNotLoaded array_diff_key($newObjectsByOid$loadedObjectsByOid);
  617.         if ($newObjectsThatWereNotLoaded) {
  618.             // Reattach NEW objects added through add(), if any.
  619.             array_walk($newObjectsThatWereNotLoaded, [$this->unwrap(), 'add']);
  620.             $this->isDirty true;
  621.         }
  622.     }
  623. }