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

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