PersistentCollection.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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, $class, Collection $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 ? 0 : 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 ? 0 : 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. }