vendor/doctrine/orm/lib/Doctrine/ORM/PersistentCollection.php line 42

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