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. * @phpstan-template TKey of array-key
  33. * @phpstan-template T
  34. * @template-extends AbstractLazyCollection<TKey,T>
  35. * @template-implements Selectable<TKey,T>
  36. * @phpstan-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. * @phpstan-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. * @phpstan-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. * @phpstan-param Collection<TKey, T>&Selectable<TKey, T> $collection The collection elements.
  93. */
  94. public function __construct(EntityManagerInterface $em, $class, Collection $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. * @phpstan-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. * @phpstan-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. * @phpstan-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. * @phpstan-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. * @phpstan-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. * @phpstan-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. }