vendor/doctrine/orm/src/UnitOfWork.php line 2926

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM;
  4. use BackedEnum;
  5. use DateTimeInterface;
  6. use Doctrine\Common\Collections\ArrayCollection;
  7. use Doctrine\Common\Collections\Collection;
  8. use Doctrine\Common\EventManager;
  9. use Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
  10. use Doctrine\DBAL\LockMode;
  11. use Doctrine\Deprecations\Deprecation;
  12. use Doctrine\ORM\Cache\Persister\CachedPersister;
  13. use Doctrine\ORM\Event\ListenersInvoker;
  14. use Doctrine\ORM\Event\OnFlushEventArgs;
  15. use Doctrine\ORM\Event\PostFlushEventArgs;
  16. use Doctrine\ORM\Event\PostPersistEventArgs;
  17. use Doctrine\ORM\Event\PostRemoveEventArgs;
  18. use Doctrine\ORM\Event\PostUpdateEventArgs;
  19. use Doctrine\ORM\Event\PreFlushEventArgs;
  20. use Doctrine\ORM\Event\PrePersistEventArgs;
  21. use Doctrine\ORM\Event\PreRemoveEventArgs;
  22. use Doctrine\ORM\Event\PreUpdateEventArgs;
  23. use Doctrine\ORM\Exception\EntityIdentityCollisionException;
  24. use Doctrine\ORM\Exception\ORMException;
  25. use Doctrine\ORM\Exception\UnexpectedAssociationValue;
  26. use Doctrine\ORM\Id\AssignedGenerator;
  27. use Doctrine\ORM\Internal\HydrationCompleteHandler;
  28. use Doctrine\ORM\Internal\StronglyConnectedComponents;
  29. use Doctrine\ORM\Internal\TopologicalSort;
  30. use Doctrine\ORM\Mapping\ClassMetadata;
  31. use Doctrine\ORM\Mapping\MappingException;
  32. use Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
  33. use Doctrine\ORM\Persisters\Collection\CollectionPersister;
  34. use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
  35. use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
  36. use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
  37. use Doctrine\ORM\Persisters\Entity\EntityPersister;
  38. use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
  39. use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
  40. use Doctrine\ORM\Proxy\InternalProxy;
  41. use Doctrine\ORM\Utility\IdentifierFlattener;
  42. use Doctrine\Persistence\Mapping\RuntimeReflectionService;
  43. use Doctrine\Persistence\NotifyPropertyChanged;
  44. use Doctrine\Persistence\ObjectManagerAware;
  45. use Doctrine\Persistence\PropertyChangedListener;
  46. use Exception;
  47. use InvalidArgumentException;
  48. use RuntimeException;
  49. use Symfony\Component\VarExporter\Hydrator;
  50. use UnexpectedValueException;
  51. use function array_chunk;
  52. use function array_combine;
  53. use function array_diff_key;
  54. use function array_filter;
  55. use function array_key_exists;
  56. use function array_map;
  57. use function array_merge;
  58. use function array_sum;
  59. use function array_values;
  60. use function assert;
  61. use function count;
  62. use function current;
  63. use function func_get_arg;
  64. use function func_num_args;
  65. use function get_class;
  66. use function get_debug_type;
  67. use function implode;
  68. use function in_array;
  69. use function is_array;
  70. use function is_object;
  71. use function method_exists;
  72. use function reset;
  73. use function spl_object_id;
  74. use function sprintf;
  75. use function strtolower;
  76. use const PHP_VERSION_ID;
  77. /**
  78. * The UnitOfWork is responsible for tracking changes to objects during an
  79. * "object-level" transaction and for writing out changes to the database
  80. * in the correct order.
  81. *
  82. * Internal note: This class contains highly performance-sensitive code.
  83. *
  84. * @phpstan-import-type AssociationMapping from ClassMetadata
  85. */
  86. class UnitOfWork implements PropertyChangedListener
  87. {
  88. /**
  89. * An entity is in MANAGED state when its persistence is managed by an EntityManager.
  90. */
  91. public const STATE_MANAGED = 1;
  92. /**
  93. * An entity is new if it has just been instantiated (i.e. using the "new" operator)
  94. * and is not (yet) managed by an EntityManager.
  95. */
  96. public const STATE_NEW = 2;
  97. /**
  98. * A detached entity is an instance with persistent state and identity that is not
  99. * (or no longer) associated with an EntityManager (and a UnitOfWork).
  100. */
  101. public const STATE_DETACHED = 3;
  102. /**
  103. * A removed entity instance is an instance with a persistent identity,
  104. * associated with an EntityManager, whose persistent state will be deleted
  105. * on commit.
  106. */
  107. public const STATE_REMOVED = 4;
  108. /**
  109. * Hint used to collect all primary keys of associated entities during hydration
  110. * and execute it in a dedicated query afterwards
  111. *
  112. * @see https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/dql-doctrine-query-language.html#temporarily-change-fetch-mode-in-dql
  113. */
  114. public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
  115. /**
  116. * The identity map that holds references to all managed entities that have
  117. * an identity. The entities are grouped by their class name.
  118. * Since all classes in a hierarchy must share the same identifier set,
  119. * we always take the root class name of the hierarchy.
  120. *
  121. * @var array<class-string, array<string, object>>
  122. */
  123. private $identityMap = [];
  124. /**
  125. * Map of all identifiers of managed entities.
  126. * Keys are object ids (spl_object_id).
  127. *
  128. * @var mixed[]
  129. * @phpstan-var array<int, array<string, mixed>>
  130. */
  131. private $entityIdentifiers = [];
  132. /**
  133. * Map of the original entity data of managed entities.
  134. * Keys are object ids (spl_object_id). This is used for calculating changesets
  135. * at commit time.
  136. *
  137. * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
  138. * A value will only really be copied if the value in the entity is modified
  139. * by the user.
  140. *
  141. * @phpstan-var array<int, array<string, mixed>>
  142. */
  143. private $originalEntityData = [];
  144. /**
  145. * Map of entity changes. Keys are object ids (spl_object_id).
  146. * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
  147. *
  148. * @phpstan-var array<int, array<string, array{mixed, mixed}>>
  149. */
  150. private $entityChangeSets = [];
  151. /**
  152. * The (cached) states of any known entities.
  153. * Keys are object ids (spl_object_id).
  154. *
  155. * @phpstan-var array<int, self::STATE_*>
  156. */
  157. private $entityStates = [];
  158. /**
  159. * Map of entities that are scheduled for dirty checking at commit time.
  160. * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
  161. * Keys are object ids (spl_object_id).
  162. *
  163. * @var array<class-string, array<int, mixed>>
  164. */
  165. private $scheduledForSynchronization = [];
  166. /**
  167. * A list of all pending entity insertions.
  168. *
  169. * @phpstan-var array<int, object>
  170. */
  171. private $entityInsertions = [];
  172. /**
  173. * A list of all pending entity updates.
  174. *
  175. * @phpstan-var array<int, object>
  176. */
  177. private $entityUpdates = [];
  178. /**
  179. * Any pending extra updates that have been scheduled by persisters.
  180. *
  181. * @phpstan-var array<int, array{object, array<string, array{mixed, mixed}>}>
  182. */
  183. private $extraUpdates = [];
  184. /**
  185. * A list of all pending entity deletions.
  186. *
  187. * @phpstan-var array<int, object>
  188. */
  189. private $entityDeletions = [];
  190. /**
  191. * New entities that were discovered through relationships that were not
  192. * marked as cascade-persist. During flush, this array is populated and
  193. * then pruned of any entities that were discovered through a valid
  194. * cascade-persist path. (Leftovers cause an error.)
  195. *
  196. * Keys are OIDs, payload is a two-item array describing the association
  197. * and the entity.
  198. *
  199. * @var array<int, array{AssociationMapping, object}> indexed by respective object spl_object_id()
  200. */
  201. private $nonCascadedNewDetectedEntities = [];
  202. /**
  203. * All pending collection deletions.
  204. *
  205. * @phpstan-var array<int, PersistentCollection<array-key, object>>
  206. */
  207. private $collectionDeletions = [];
  208. /**
  209. * All pending collection updates.
  210. *
  211. * @phpstan-var array<int, PersistentCollection<array-key, object>>
  212. */
  213. private $collectionUpdates = [];
  214. /**
  215. * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
  216. * At the end of the UnitOfWork all these collections will make new snapshots
  217. * of their data.
  218. *
  219. * @phpstan-var array<int, PersistentCollection<array-key, object>>
  220. */
  221. private $visitedCollections = [];
  222. /**
  223. * List of collections visited during the changeset calculation that contain to-be-removed
  224. * entities and need to have keys removed post commit.
  225. *
  226. * Indexed by Collection object ID, which also serves as the key in self::$visitedCollections;
  227. * values are the key names that need to be removed.
  228. *
  229. * @phpstan-var array<int, array<array-key, true>>
  230. */
  231. private $pendingCollectionElementRemovals = [];
  232. /**
  233. * The EntityManager that "owns" this UnitOfWork instance.
  234. *
  235. * @var EntityManagerInterface
  236. */
  237. private $em;
  238. /**
  239. * The entity persister instances used to persist entity instances.
  240. *
  241. * @phpstan-var array<string, EntityPersister>
  242. */
  243. private $persisters = [];
  244. /**
  245. * The collection persister instances used to persist collections.
  246. *
  247. * @phpstan-var array<array-key, CollectionPersister>
  248. */
  249. private $collectionPersisters = [];
  250. /**
  251. * The EventManager used for dispatching events.
  252. *
  253. * @var EventManager
  254. */
  255. private $evm;
  256. /**
  257. * The ListenersInvoker used for dispatching events.
  258. *
  259. * @var ListenersInvoker
  260. */
  261. private $listenersInvoker;
  262. /**
  263. * The IdentifierFlattener used for manipulating identifiers
  264. *
  265. * @var IdentifierFlattener
  266. */
  267. private $identifierFlattener;
  268. /**
  269. * Orphaned entities that are scheduled for removal.
  270. *
  271. * @phpstan-var array<int, object>
  272. */
  273. private $orphanRemovals = [];
  274. /**
  275. * Read-Only objects are never evaluated
  276. *
  277. * @var array<int, true>
  278. */
  279. private $readOnlyObjects = [];
  280. /**
  281. * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
  282. *
  283. * @var array<class-string, array<string, mixed>>
  284. */
  285. private $eagerLoadingEntities = [];
  286. /** @var array<string, array<string, mixed>> */
  287. private $eagerLoadingCollections = [];
  288. /** @var bool */
  289. protected $hasCache = false;
  290. /**
  291. * Helper for handling completion of hydration
  292. *
  293. * @var HydrationCompleteHandler
  294. */
  295. private $hydrationCompleteHandler;
  296. /** @var ReflectionPropertiesGetter */
  297. private $reflectionPropertiesGetter;
  298. /**
  299. * Initializes a new UnitOfWork instance, bound to the given EntityManager.
  300. */
  301. public function __construct(EntityManagerInterface $em)
  302. {
  303. $this->em = $em;
  304. $this->evm = $em->getEventManager();
  305. $this->listenersInvoker = new ListenersInvoker($em);
  306. $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled();
  307. $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory());
  308. $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
  309. $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
  310. }
  311. /**
  312. * Commits the UnitOfWork, executing all operations that have been postponed
  313. * up to this point. The state of all managed entities will be synchronized with
  314. * the database.
  315. *
  316. * The operations are executed in the following order:
  317. *
  318. * 1) All entity insertions
  319. * 2) All entity updates
  320. * 3) All collection deletions
  321. * 4) All collection updates
  322. * 5) All entity deletions
  323. *
  324. * @param object|mixed[]|null $entity
  325. *
  326. * @return void
  327. *
  328. * @throws Exception
  329. */
  330. public function commit($entity = null)
  331. {
  332. if ($entity !== null) {
  333. Deprecation::triggerIfCalledFromOutside(
  334. 'doctrine/orm',
  335. 'https://github.com/doctrine/orm/issues/8459',
  336. 'Calling %s() with any arguments to commit specific entities is deprecated and will not be supported in Doctrine ORM 3.0.',
  337. __METHOD__
  338. );
  339. }
  340. $connection = $this->em->getConnection();
  341. if ($connection instanceof PrimaryReadReplicaConnection) {
  342. $connection->ensureConnectedToPrimary();
  343. }
  344. // Raise preFlush
  345. if ($this->evm->hasListeners(Events::preFlush)) {
  346. $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
  347. }
  348. // Compute changes done since last commit.
  349. if ($entity === null) {
  350. $this->computeChangeSets();
  351. } elseif (is_object($entity)) {
  352. $this->computeSingleEntityChangeSet($entity);
  353. } elseif (is_array($entity)) {
  354. foreach ($entity as $object) {
  355. $this->computeSingleEntityChangeSet($object);
  356. }
  357. }
  358. if (
  359. ! ($this->entityInsertions ||
  360. $this->entityDeletions ||
  361. $this->entityUpdates ||
  362. $this->collectionUpdates ||
  363. $this->collectionDeletions ||
  364. $this->orphanRemovals)
  365. ) {
  366. $this->dispatchOnFlushEvent();
  367. $this->dispatchPostFlushEvent();
  368. $this->postCommitCleanup($entity);
  369. return; // Nothing to do.
  370. }
  371. $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
  372. if ($this->orphanRemovals) {
  373. foreach ($this->orphanRemovals as $orphan) {
  374. $this->remove($orphan);
  375. }
  376. }
  377. $this->dispatchOnFlushEvent();
  378. $conn = $this->em->getConnection();
  379. $conn->beginTransaction();
  380. $successful = false;
  381. try {
  382. // Collection deletions (deletions of complete collections)
  383. foreach ($this->collectionDeletions as $collectionToDelete) {
  384. // Deferred explicit tracked collections can be removed only when owning relation was persisted
  385. $owner = $collectionToDelete->getOwner();
  386. if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
  387. $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
  388. }
  389. }
  390. if ($this->entityInsertions) {
  391. // Perform entity insertions first, so that all new entities have their rows in the database
  392. // and can be referred to by foreign keys. The commit order only needs to take new entities
  393. // into account (new entities referring to other new entities), since all other types (entities
  394. // with updates or scheduled deletions) are currently not a problem, since they are already
  395. // in the database.
  396. $this->executeInserts();
  397. }
  398. if ($this->entityUpdates) {
  399. // Updates do not need to follow a particular order
  400. $this->executeUpdates();
  401. }
  402. // Extra updates that were requested by persisters.
  403. // This may include foreign keys that could not be set when an entity was inserted,
  404. // which may happen in the case of circular foreign key relationships.
  405. if ($this->extraUpdates) {
  406. $this->executeExtraUpdates();
  407. }
  408. // Collection updates (deleteRows, updateRows, insertRows)
  409. // No particular order is necessary, since all entities themselves are already
  410. // in the database
  411. foreach ($this->collectionUpdates as $collectionToUpdate) {
  412. $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
  413. }
  414. // Entity deletions come last. Their order only needs to take care of other deletions
  415. // (first delete entities depending upon others, before deleting depended-upon entities).
  416. if ($this->entityDeletions) {
  417. $this->executeDeletions();
  418. }
  419. // Commit failed silently
  420. if ($conn->commit() === false) {
  421. $object = is_object($entity) ? $entity : null;
  422. throw new OptimisticLockException('Commit failed', $object);
  423. }
  424. $successful = true;
  425. } finally {
  426. if (! $successful) {
  427. $this->em->close();
  428. if ($conn->isTransactionActive()) {
  429. $conn->rollBack();
  430. }
  431. $this->afterTransactionRolledBack();
  432. }
  433. }
  434. $this->afterTransactionComplete();
  435. // Unset removed entities from collections, and take new snapshots from
  436. // all visited collections.
  437. foreach ($this->visitedCollections as $coid => $coll) {
  438. if (isset($this->pendingCollectionElementRemovals[$coid])) {
  439. foreach ($this->pendingCollectionElementRemovals[$coid] as $key => $valueIgnored) {
  440. unset($coll[$key]);
  441. }
  442. }
  443. $coll->takeSnapshot();
  444. }
  445. $this->dispatchPostFlushEvent();
  446. $this->postCommitCleanup($entity);
  447. }
  448. /** @param object|object[]|null $entity */
  449. private function postCommitCleanup($entity): void
  450. {
  451. $this->entityInsertions =
  452. $this->entityUpdates =
  453. $this->entityDeletions =
  454. $this->extraUpdates =
  455. $this->collectionUpdates =
  456. $this->nonCascadedNewDetectedEntities =
  457. $this->collectionDeletions =
  458. $this->pendingCollectionElementRemovals =
  459. $this->visitedCollections =
  460. $this->orphanRemovals = [];
  461. if ($entity === null) {
  462. $this->entityChangeSets = $this->scheduledForSynchronization = [];
  463. return;
  464. }
  465. $entities = is_object($entity)
  466. ? [$entity]
  467. : $entity;
  468. foreach ($entities as $object) {
  469. $oid = spl_object_id($object);
  470. $this->clearEntityChangeSet($oid);
  471. unset($this->scheduledForSynchronization[$this->em->getClassMetadata(get_class($object))->rootEntityName][$oid]);
  472. }
  473. }
  474. /**
  475. * Computes the changesets of all entities scheduled for insertion.
  476. */
  477. private function computeScheduleInsertsChangeSets(): void
  478. {
  479. foreach ($this->entityInsertions as $entity) {
  480. $class = $this->em->getClassMetadata(get_class($entity));
  481. $this->computeChangeSet($class, $entity);
  482. }
  483. }
  484. /**
  485. * Only flushes the given entity according to a ruleset that keeps the UoW consistent.
  486. *
  487. * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well!
  488. * 2. Read Only entities are skipped.
  489. * 3. Proxies are skipped.
  490. * 4. Only if entity is properly managed.
  491. *
  492. * @param object $entity
  493. *
  494. * @throws InvalidArgumentException
  495. */
  496. private function computeSingleEntityChangeSet($entity): void
  497. {
  498. $state = $this->getEntityState($entity);
  499. if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
  500. throw new InvalidArgumentException('Entity has to be managed or scheduled for removal for single computation ' . self::objToStr($entity));
  501. }
  502. $class = $this->em->getClassMetadata(get_class($entity));
  503. if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
  504. $this->persist($entity);
  505. }
  506. // Compute changes for INSERTed entities first. This must always happen even in this case.
  507. $this->computeScheduleInsertsChangeSets();
  508. if ($class->isReadOnly) {
  509. return;
  510. }
  511. // Ignore uninitialized proxy objects
  512. if ($this->isUninitializedObject($entity)) {
  513. return;
  514. }
  515. // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
  516. $oid = spl_object_id($entity);
  517. if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
  518. $this->computeChangeSet($class, $entity);
  519. }
  520. }
  521. /**
  522. * Executes any extra updates that have been scheduled.
  523. */
  524. private function executeExtraUpdates(): void
  525. {
  526. foreach ($this->extraUpdates as $oid => $update) {
  527. [$entity, $changeset] = $update;
  528. $this->entityChangeSets[$oid] = $changeset;
  529. $this->getEntityPersister(get_class($entity))->update($entity);
  530. }
  531. $this->extraUpdates = [];
  532. }
  533. /**
  534. * Gets the changeset for an entity.
  535. *
  536. * @param object $entity
  537. *
  538. * @return mixed[][]
  539. * @phpstan-return array<string, array{mixed, mixed}|PersistentCollection>
  540. */
  541. public function & getEntityChangeSet($entity)
  542. {
  543. $oid = spl_object_id($entity);
  544. $data = [];
  545. if (! isset($this->entityChangeSets[$oid])) {
  546. return $data;
  547. }
  548. return $this->entityChangeSets[$oid];
  549. }
  550. /**
  551. * Computes the changes that happened to a single entity.
  552. *
  553. * Modifies/populates the following properties:
  554. *
  555. * {@link _originalEntityData}
  556. * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
  557. * then it was not fetched from the database and therefore we have no original
  558. * entity data yet. All of the current entity data is stored as the original entity data.
  559. *
  560. * {@link _entityChangeSets}
  561. * The changes detected on all properties of the entity are stored there.
  562. * A change is a tuple array where the first entry is the old value and the second
  563. * entry is the new value of the property. Changesets are used by persisters
  564. * to INSERT/UPDATE the persistent entity state.
  565. *
  566. * {@link _entityUpdates}
  567. * If the entity is already fully MANAGED (has been fetched from the database before)
  568. * and any changes to its properties are detected, then a reference to the entity is stored
  569. * there to mark it for an update.
  570. *
  571. * {@link _collectionDeletions}
  572. * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
  573. * then this collection is marked for deletion.
  574. *
  575. * @param ClassMetadata $class The class descriptor of the entity.
  576. * @param object $entity The entity for which to compute the changes.
  577. * @phpstan-param ClassMetadata<T> $class
  578. * @phpstan-param T $entity
  579. *
  580. * @return void
  581. *
  582. * @template T of object
  583. *
  584. * @ignore
  585. */
  586. public function computeChangeSet(ClassMetadata $class, $entity)
  587. {
  588. $oid = spl_object_id($entity);
  589. if (isset($this->readOnlyObjects[$oid])) {
  590. return;
  591. }
  592. if (! $class->isInheritanceTypeNone()) {
  593. $class = $this->em->getClassMetadata(get_class($entity));
  594. }
  595. $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
  596. if ($invoke !== ListenersInvoker::INVOKE_NONE) {
  597. $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
  598. }
  599. $actualData = [];
  600. foreach ($class->reflFields as $name => $refProp) {
  601. $value = $refProp->getValue($entity);
  602. if ($class->isCollectionValuedAssociation($name) && $value !== null) {
  603. if ($value instanceof PersistentCollection) {
  604. if ($value->getOwner() === $entity) {
  605. $actualData[$name] = $value;
  606. continue;
  607. }
  608. $value = new ArrayCollection($value->getValues());
  609. }
  610. // If $value is not a Collection then use an ArrayCollection.
  611. if (! $value instanceof Collection) {
  612. $value = new ArrayCollection($value);
  613. }
  614. $assoc = $class->associationMappings[$name];
  615. // Inject PersistentCollection
  616. $value = new PersistentCollection(
  617. $this->em,
  618. $this->em->getClassMetadata($assoc['targetEntity']),
  619. $value
  620. );
  621. $value->setOwner($entity, $assoc);
  622. $value->setDirty(! $value->isEmpty());
  623. $refProp->setValue($entity, $value);
  624. $actualData[$name] = $value;
  625. continue;
  626. }
  627. if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) {
  628. $actualData[$name] = $value;
  629. }
  630. }
  631. if (! isset($this->originalEntityData[$oid])) {
  632. // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
  633. // These result in an INSERT.
  634. $this->originalEntityData[$oid] = $actualData;
  635. $changeSet = [];
  636. foreach ($actualData as $propName => $actualValue) {
  637. if (! isset($class->associationMappings[$propName])) {
  638. $changeSet[$propName] = [null, $actualValue];
  639. continue;
  640. }
  641. $assoc = $class->associationMappings[$propName];
  642. if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
  643. $changeSet[$propName] = [null, $actualValue];
  644. }
  645. }
  646. $this->entityChangeSets[$oid] = $changeSet;
  647. } else {
  648. // Entity is "fully" MANAGED: it was already fully persisted before
  649. // and we have a copy of the original data
  650. $originalData = $this->originalEntityData[$oid];
  651. $isChangeTrackingNotify = $class->isChangeTrackingNotify();
  652. $changeSet = $isChangeTrackingNotify && isset($this->entityChangeSets[$oid])
  653. ? $this->entityChangeSets[$oid]
  654. : [];
  655. foreach ($actualData as $propName => $actualValue) {
  656. // skip field, its a partially omitted one!
  657. if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
  658. continue;
  659. }
  660. $orgValue = $originalData[$propName];
  661. if (! empty($class->fieldMappings[$propName]['enumType'])) {
  662. if (is_array($orgValue)) {
  663. foreach ($orgValue as $id => $val) {
  664. if ($val instanceof BackedEnum) {
  665. $orgValue[$id] = $val->value;
  666. }
  667. }
  668. } else {
  669. if ($orgValue instanceof BackedEnum) {
  670. $orgValue = $orgValue->value;
  671. }
  672. }
  673. }
  674. // skip if value haven't changed
  675. if ($orgValue === $actualValue) {
  676. continue;
  677. }
  678. // if regular field
  679. if (! isset($class->associationMappings[$propName])) {
  680. if ($isChangeTrackingNotify) {
  681. continue;
  682. }
  683. $changeSet[$propName] = [$orgValue, $actualValue];
  684. continue;
  685. }
  686. $assoc = $class->associationMappings[$propName];
  687. // Persistent collection was exchanged with the "originally"
  688. // created one. This can only mean it was cloned and replaced
  689. // on another entity.
  690. if ($actualValue instanceof PersistentCollection) {
  691. $owner = $actualValue->getOwner();
  692. if ($owner === null) { // cloned
  693. $actualValue->setOwner($entity, $assoc);
  694. } elseif ($owner !== $entity) { // no clone, we have to fix
  695. if (! $actualValue->isInitialized()) {
  696. $actualValue->initialize(); // we have to do this otherwise the cols share state
  697. }
  698. $newValue = clone $actualValue;
  699. $newValue->setOwner($entity, $assoc);
  700. $class->reflFields[$propName]->setValue($entity, $newValue);
  701. }
  702. }
  703. if ($orgValue instanceof PersistentCollection) {
  704. // A PersistentCollection was de-referenced, so delete it.
  705. $coid = spl_object_id($orgValue);
  706. if (isset($this->collectionDeletions[$coid])) {
  707. continue;
  708. }
  709. $this->collectionDeletions[$coid] = $orgValue;
  710. $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored.
  711. continue;
  712. }
  713. if ($assoc['type'] & ClassMetadata::TO_ONE) {
  714. if ($assoc['isOwningSide']) {
  715. $changeSet[$propName] = [$orgValue, $actualValue];
  716. }
  717. if ($orgValue !== null && $assoc['orphanRemoval']) {
  718. assert(is_object($orgValue));
  719. $this->scheduleOrphanRemoval($orgValue);
  720. }
  721. }
  722. }
  723. if ($changeSet) {
  724. $this->entityChangeSets[$oid] = $changeSet;
  725. $this->originalEntityData[$oid] = $actualData;
  726. $this->entityUpdates[$oid] = $entity;
  727. }
  728. }
  729. // Look for changes in associations of the entity
  730. foreach ($class->associationMappings as $field => $assoc) {
  731. $val = $class->reflFields[$field]->getValue($entity);
  732. if ($val === null) {
  733. continue;
  734. }
  735. $this->computeAssociationChanges($assoc, $val);
  736. if (
  737. ! isset($this->entityChangeSets[$oid]) &&
  738. $assoc['isOwningSide'] &&
  739. $assoc['type'] === ClassMetadata::MANY_TO_MANY &&
  740. $val instanceof PersistentCollection &&
  741. $val->isDirty()
  742. ) {
  743. $this->entityChangeSets[$oid] = [];
  744. $this->originalEntityData[$oid] = $actualData;
  745. $this->entityUpdates[$oid] = $entity;
  746. }
  747. }
  748. }
  749. /**
  750. * Computes all the changes that have been done to entities and collections
  751. * since the last commit and stores these changes in the _entityChangeSet map
  752. * temporarily for access by the persisters, until the UoW commit is finished.
  753. *
  754. * @return void
  755. */
  756. public function computeChangeSets()
  757. {
  758. // Compute changes for INSERTed entities first. This must always happen.
  759. $this->computeScheduleInsertsChangeSets();
  760. // Compute changes for other MANAGED entities. Change tracking policies take effect here.
  761. foreach ($this->identityMap as $className => $entities) {
  762. $class = $this->em->getClassMetadata($className);
  763. // Skip class if instances are read-only
  764. if ($class->isReadOnly) {
  765. continue;
  766. }
  767. // If change tracking is explicit or happens through notification, then only compute
  768. // changes on entities of that type that are explicitly marked for synchronization.
  769. switch (true) {
  770. case $class->isChangeTrackingDeferredImplicit():
  771. $entitiesToProcess = $entities;
  772. break;
  773. case isset($this->scheduledForSynchronization[$className]):
  774. $entitiesToProcess = $this->scheduledForSynchronization[$className];
  775. break;
  776. default:
  777. $entitiesToProcess = [];
  778. }
  779. foreach ($entitiesToProcess as $entity) {
  780. // Ignore uninitialized proxy objects
  781. if ($this->isUninitializedObject($entity)) {
  782. continue;
  783. }
  784. // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
  785. $oid = spl_object_id($entity);
  786. if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
  787. $this->computeChangeSet($class, $entity);
  788. }
  789. }
  790. }
  791. }
  792. /**
  793. * Computes the changes of an association.
  794. *
  795. * @param mixed $value The value of the association.
  796. * @phpstan-param AssociationMapping $assoc The association mapping.
  797. *
  798. * @throws ORMInvalidArgumentException
  799. * @throws ORMException
  800. */
  801. private function computeAssociationChanges(array $assoc, $value): void
  802. {
  803. if ($this->isUninitializedObject($value)) {
  804. return;
  805. }
  806. // If this collection is dirty, schedule it for updates
  807. if ($value instanceof PersistentCollection && $value->isDirty()) {
  808. $coid = spl_object_id($value);
  809. $this->collectionUpdates[$coid] = $value;
  810. $this->visitedCollections[$coid] = $value;
  811. }
  812. // Look through the entities, and in any of their associations,
  813. // for transient (new) entities, recursively. ("Persistence by reachability")
  814. // Unwrap. Uninitialized collections will simply be empty.
  815. $unwrappedValue = $assoc['type'] & ClassMetadata::TO_ONE ? [$value] : $value->unwrap();
  816. $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
  817. foreach ($unwrappedValue as $key => $entry) {
  818. if (! ($entry instanceof $targetClass->name)) {
  819. throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
  820. }
  821. $state = $this->getEntityState($entry, self::STATE_NEW);
  822. if (! ($entry instanceof $assoc['targetEntity'])) {
  823. throw UnexpectedAssociationValue::create(
  824. $assoc['sourceEntity'],
  825. $assoc['fieldName'],
  826. get_debug_type($entry),
  827. $assoc['targetEntity']
  828. );
  829. }
  830. switch ($state) {
  831. case self::STATE_NEW:
  832. if (! $assoc['isCascadePersist']) {
  833. /*
  834. * For now just record the details, because this may
  835. * not be an issue if we later discover another pathway
  836. * through the object-graph where cascade-persistence
  837. * is enabled for this object.
  838. */
  839. $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$assoc, $entry];
  840. break;
  841. }
  842. $this->persistNew($targetClass, $entry);
  843. $this->computeChangeSet($targetClass, $entry);
  844. break;
  845. case self::STATE_REMOVED:
  846. // Consume the $value as array (it's either an array or an ArrayAccess)
  847. // and remove the element from Collection.
  848. if (! ($assoc['type'] & ClassMetadata::TO_MANY)) {
  849. break;
  850. }
  851. $coid = spl_object_id($value);
  852. $this->visitedCollections[$coid] = $value;
  853. if (! isset($this->pendingCollectionElementRemovals[$coid])) {
  854. $this->pendingCollectionElementRemovals[$coid] = [];
  855. }
  856. $this->pendingCollectionElementRemovals[$coid][$key] = true;
  857. break;
  858. case self::STATE_DETACHED:
  859. // Can actually not happen right now as we assume STATE_NEW,
  860. // so the exception will be raised from the DBAL layer (constraint violation).
  861. throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
  862. default:
  863. // MANAGED associated entities are already taken into account
  864. // during changeset calculation anyway, since they are in the identity map.
  865. }
  866. }
  867. }
  868. /**
  869. * @param object $entity
  870. * @phpstan-param ClassMetadata<T> $class
  871. * @phpstan-param T $entity
  872. *
  873. * @template T of object
  874. */
  875. private function persistNew(ClassMetadata $class, $entity): void
  876. {
  877. $oid = spl_object_id($entity);
  878. $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
  879. if ($invoke !== ListenersInvoker::INVOKE_NONE) {
  880. $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new PrePersistEventArgs($entity, $this->em), $invoke);
  881. }
  882. $idGen = $class->idGenerator;
  883. if (! $idGen->isPostInsertGenerator()) {
  884. $idValue = $idGen->generateId($this->em, $entity);
  885. if (! $idGen instanceof AssignedGenerator) {
  886. $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
  887. $class->setIdentifierValues($entity, $idValue);
  888. }
  889. // Some identifiers may be foreign keys to new entities.
  890. // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
  891. if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
  892. $this->entityIdentifiers[$oid] = $idValue;
  893. }
  894. }
  895. $this->entityStates[$oid] = self::STATE_MANAGED;
  896. if (! isset($this->entityInsertions[$oid])) {
  897. $this->scheduleForInsert($entity);
  898. }
  899. }
  900. /** @param mixed[] $idValue */
  901. private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue): bool
  902. {
  903. foreach ($idValue as $idField => $idFieldValue) {
  904. if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
  905. return true;
  906. }
  907. }
  908. return false;
  909. }
  910. /**
  911. * INTERNAL:
  912. * Computes the changeset of an individual entity, independently of the
  913. * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
  914. *
  915. * The passed entity must be a managed entity. If the entity already has a change set
  916. * because this method is invoked during a commit cycle then the change sets are added.
  917. * whereby changes detected in this method prevail.
  918. *
  919. * @param ClassMetadata $class The class descriptor of the entity.
  920. * @param object $entity The entity for which to (re)calculate the change set.
  921. * @phpstan-param ClassMetadata<T> $class
  922. * @phpstan-param T $entity
  923. *
  924. * @return void
  925. *
  926. * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
  927. *
  928. * @template T of object
  929. * @ignore
  930. */
  931. public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
  932. {
  933. $oid = spl_object_id($entity);
  934. if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
  935. throw ORMInvalidArgumentException::entityNotManaged($entity);
  936. }
  937. // skip if change tracking is "NOTIFY"
  938. if ($class->isChangeTrackingNotify()) {
  939. return;
  940. }
  941. if (! $class->isInheritanceTypeNone()) {
  942. $class = $this->em->getClassMetadata(get_class($entity));
  943. }
  944. $actualData = [];
  945. foreach ($class->reflFields as $name => $refProp) {
  946. if (
  947. ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity())
  948. && ($name !== $class->versionField)
  949. && ! $class->isCollectionValuedAssociation($name)
  950. ) {
  951. $actualData[$name] = $refProp->getValue($entity);
  952. }
  953. }
  954. if (! isset($this->originalEntityData[$oid])) {
  955. throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
  956. }
  957. $originalData = $this->originalEntityData[$oid];
  958. $changeSet = [];
  959. foreach ($actualData as $propName => $actualValue) {
  960. $orgValue = $originalData[$propName] ?? null;
  961. if (isset($class->fieldMappings[$propName]['enumType'])) {
  962. if (is_array($orgValue)) {
  963. foreach ($orgValue as $id => $val) {
  964. if ($val instanceof BackedEnum) {
  965. $orgValue[$id] = $val->value;
  966. }
  967. }
  968. } else {
  969. if ($orgValue instanceof BackedEnum) {
  970. $orgValue = $orgValue->value;
  971. }
  972. }
  973. }
  974. if ($orgValue !== $actualValue) {
  975. $changeSet[$propName] = [$orgValue, $actualValue];
  976. }
  977. }
  978. if ($changeSet) {
  979. if (isset($this->entityChangeSets[$oid])) {
  980. $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
  981. } elseif (! isset($this->entityInsertions[$oid])) {
  982. $this->entityChangeSets[$oid] = $changeSet;
  983. $this->entityUpdates[$oid] = $entity;
  984. }
  985. $this->originalEntityData[$oid] = $actualData;
  986. }
  987. }
  988. /**
  989. * Executes entity insertions
  990. */
  991. private function executeInserts(): void
  992. {
  993. $entities = $this->computeInsertExecutionOrder();
  994. $eventsToDispatch = [];
  995. foreach ($entities as $entity) {
  996. $oid = spl_object_id($entity);
  997. $class = $this->em->getClassMetadata(get_class($entity));
  998. $persister = $this->getEntityPersister($class->name);
  999. $persister->addInsert($entity);
  1000. unset($this->entityInsertions[$oid]);
  1001. $postInsertIds = $persister->executeInserts();
  1002. if (is_array($postInsertIds)) {
  1003. Deprecation::trigger(
  1004. 'doctrine/orm',
  1005. 'https://github.com/doctrine/orm/pull/10743/',
  1006. 'Returning post insert IDs from \Doctrine\ORM\Persisters\Entity\EntityPersister::executeInserts() is deprecated and will not be supported in Doctrine ORM 3.0. Make the persister call Doctrine\ORM\UnitOfWork::assignPostInsertId() instead.'
  1007. );
  1008. // Persister returned post-insert IDs
  1009. foreach ($postInsertIds as $postInsertId) {
  1010. $this->assignPostInsertId($postInsertId['entity'], $postInsertId['generatedId']);
  1011. }
  1012. }
  1013. if (! isset($this->entityIdentifiers[$oid])) {
  1014. //entity was not added to identity map because some identifiers are foreign keys to new entities.
  1015. //add it now
  1016. $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
  1017. }
  1018. $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
  1019. if ($invoke !== ListenersInvoker::INVOKE_NONE) {
  1020. $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
  1021. }
  1022. }
  1023. // Defer dispatching `postPersist` events to until all entities have been inserted and post-insert
  1024. // IDs have been assigned.
  1025. foreach ($eventsToDispatch as $event) {
  1026. $this->listenersInvoker->invoke(
  1027. $event['class'],
  1028. Events::postPersist,
  1029. $event['entity'],
  1030. new PostPersistEventArgs($event['entity'], $this->em),
  1031. $event['invoke']
  1032. );
  1033. }
  1034. }
  1035. /**
  1036. * @param object $entity
  1037. * @phpstan-param ClassMetadata<T> $class
  1038. * @phpstan-param T $entity
  1039. *
  1040. * @template T of object
  1041. */
  1042. private function addToEntityIdentifiersAndEntityMap(
  1043. ClassMetadata $class,
  1044. int $oid,
  1045. $entity
  1046. ): void {
  1047. $identifier = [];
  1048. foreach ($class->getIdentifierFieldNames() as $idField) {
  1049. $origValue = $class->getFieldValue($entity, $idField);
  1050. $value = null;
  1051. if (isset($class->associationMappings[$idField])) {
  1052. // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
  1053. $value = $this->getSingleIdentifierValue($origValue);
  1054. }
  1055. $identifier[$idField] = $value ?? $origValue;
  1056. $this->originalEntityData[$oid][$idField] = $origValue;
  1057. }
  1058. $this->entityStates[$oid] = self::STATE_MANAGED;
  1059. $this->entityIdentifiers[$oid] = $identifier;
  1060. $this->addToIdentityMap($entity);
  1061. }
  1062. /**
  1063. * Executes all entity updates
  1064. */
  1065. private function executeUpdates(): void
  1066. {
  1067. foreach ($this->entityUpdates as $oid => $entity) {
  1068. $class = $this->em->getClassMetadata(get_class($entity));
  1069. $persister = $this->getEntityPersister($class->name);
  1070. $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
  1071. $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
  1072. if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
  1073. $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
  1074. $this->recomputeSingleEntityChangeSet($class, $entity);
  1075. }
  1076. if (! empty($this->entityChangeSets[$oid])) {
  1077. $persister->update($entity);
  1078. }
  1079. unset($this->entityUpdates[$oid]);
  1080. if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
  1081. $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new PostUpdateEventArgs($entity, $this->em), $postUpdateInvoke);
  1082. }
  1083. }
  1084. }
  1085. /**
  1086. * Executes all entity deletions
  1087. */
  1088. private function executeDeletions(): void
  1089. {
  1090. $entities = $this->computeDeleteExecutionOrder();
  1091. $eventsToDispatch = [];
  1092. foreach ($entities as $entity) {
  1093. $this->removeFromIdentityMap($entity);
  1094. $oid = spl_object_id($entity);
  1095. $class = $this->em->getClassMetadata(get_class($entity));
  1096. $persister = $this->getEntityPersister($class->name);
  1097. $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
  1098. $persister->delete($entity);
  1099. unset(
  1100. $this->entityDeletions[$oid],
  1101. $this->entityIdentifiers[$oid],
  1102. $this->originalEntityData[$oid],
  1103. $this->entityStates[$oid]
  1104. );
  1105. // Entity with this $oid after deletion treated as NEW, even if the $oid
  1106. // is obtained by a new entity because the old one went out of scope.
  1107. //$this->entityStates[$oid] = self::STATE_NEW;
  1108. if (! $class->isIdentifierNatural()) {
  1109. $class->reflFields[$class->identifier[0]]->setValue($entity, null);
  1110. }
  1111. if ($invoke !== ListenersInvoker::INVOKE_NONE) {
  1112. $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
  1113. }
  1114. }
  1115. // Defer dispatching `postRemove` events to until all entities have been removed.
  1116. foreach ($eventsToDispatch as $event) {
  1117. $this->listenersInvoker->invoke(
  1118. $event['class'],
  1119. Events::postRemove,
  1120. $event['entity'],
  1121. new PostRemoveEventArgs($event['entity'], $this->em),
  1122. $event['invoke']
  1123. );
  1124. }
  1125. }
  1126. /** @return list<object> */
  1127. private function computeInsertExecutionOrder(): array
  1128. {
  1129. $sort = new TopologicalSort();
  1130. // First make sure we have all the nodes
  1131. foreach ($this->entityInsertions as $entity) {
  1132. $sort->addNode($entity);
  1133. }
  1134. // Now add edges
  1135. foreach ($this->entityInsertions as $entity) {
  1136. $class = $this->em->getClassMetadata(get_class($entity));
  1137. foreach ($class->associationMappings as $assoc) {
  1138. // We only need to consider the owning sides of to-one associations,
  1139. // since many-to-many associations are persisted at a later step and
  1140. // have no insertion order problems (all entities already in the database
  1141. // at that time).
  1142. if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
  1143. continue;
  1144. }
  1145. $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
  1146. // If there is no entity that we need to refer to, or it is already in the
  1147. // database (i. e. does not have to be inserted), no need to consider it.
  1148. if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
  1149. continue;
  1150. }
  1151. // An entity that references back to itself _and_ uses an application-provided ID
  1152. // (the "NONE" generator strategy) can be exempted from commit order computation.
  1153. // See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case.
  1154. // A non-NULLable self-reference would be a cycle in the graph.
  1155. if ($targetEntity === $entity && $class->isIdentifierNatural()) {
  1156. continue;
  1157. }
  1158. // According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn,
  1159. // the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other
  1160. // level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well.
  1161. //
  1162. // Same in \Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns,
  1163. // to give two examples.
  1164. assert(isset($assoc['joinColumns']));
  1165. $joinColumns = reset($assoc['joinColumns']);
  1166. $isNullable = ! isset($joinColumns['nullable']) || $joinColumns['nullable'];
  1167. // Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
  1168. // topological sort result will output the depended-upon nodes first, which means we can insert
  1169. // entities in that order.
  1170. $sort->addEdge($entity, $targetEntity, $isNullable);
  1171. }
  1172. }
  1173. return $sort->sort();
  1174. }
  1175. /** @return list<object> */
  1176. private function computeDeleteExecutionOrder(): array
  1177. {
  1178. $stronglyConnectedComponents = new StronglyConnectedComponents();
  1179. $sort = new TopologicalSort();
  1180. foreach ($this->entityDeletions as $entity) {
  1181. $stronglyConnectedComponents->addNode($entity);
  1182. $sort->addNode($entity);
  1183. }
  1184. // First, consider only "on delete cascade" associations between entities
  1185. // and find strongly connected groups. Once we delete any one of the entities
  1186. // in such a group, _all_ of the other entities will be removed as well. So,
  1187. // we need to treat those groups like a single entity when performing delete
  1188. // order topological sorting.
  1189. foreach ($this->entityDeletions as $entity) {
  1190. $class = $this->em->getClassMetadata(get_class($entity));
  1191. foreach ($class->associationMappings as $assoc) {
  1192. // We only need to consider the owning sides of to-one associations,
  1193. // since many-to-many associations can always be (and have already been)
  1194. // deleted in a preceding step.
  1195. if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
  1196. continue;
  1197. }
  1198. assert(isset($assoc['joinColumns']));
  1199. $joinColumns = reset($assoc['joinColumns']);
  1200. if (! isset($joinColumns['onDelete'])) {
  1201. continue;
  1202. }
  1203. $onDeleteOption = strtolower($joinColumns['onDelete']);
  1204. if ($onDeleteOption !== 'cascade') {
  1205. continue;
  1206. }
  1207. $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
  1208. // If the association does not refer to another entity or that entity
  1209. // is not to be deleted, there is no ordering problem and we can
  1210. // skip this particular association.
  1211. if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
  1212. continue;
  1213. }
  1214. $stronglyConnectedComponents->addEdge($entity, $targetEntity);
  1215. }
  1216. }
  1217. $stronglyConnectedComponents->findStronglyConnectedComponents();
  1218. // Now do the actual topological sorting to find the delete order.
  1219. foreach ($this->entityDeletions as $entity) {
  1220. $class = $this->em->getClassMetadata(get_class($entity));
  1221. // Get the entities representing the SCC
  1222. $entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
  1223. // When $entity is part of a non-trivial strongly connected component group
  1224. // (a group containing not only those entities alone), make sure we process it _after_ the
  1225. // entity representing the group.
  1226. // The dependency direction implies that "$entity depends on $entityComponent
  1227. // being deleted first". The topological sort will output the depended-upon nodes first.
  1228. if ($entityComponent !== $entity) {
  1229. $sort->addEdge($entity, $entityComponent, false);
  1230. }
  1231. foreach ($class->associationMappings as $assoc) {
  1232. // We only need to consider the owning sides of to-one associations,
  1233. // since many-to-many associations can always be (and have already been)
  1234. // deleted in a preceding step.
  1235. if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
  1236. continue;
  1237. }
  1238. // For associations that implement a database-level set null operation,
  1239. // we do not have to follow a particular order: If the referred-to entity is
  1240. // deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
  1241. // So, we can skip it in the computation.
  1242. assert(isset($assoc['joinColumns']));
  1243. $joinColumns = reset($assoc['joinColumns']);
  1244. if (isset($joinColumns['onDelete'])) {
  1245. $onDeleteOption = strtolower($joinColumns['onDelete']);
  1246. if ($onDeleteOption === 'set null') {
  1247. continue;
  1248. }
  1249. }
  1250. $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
  1251. // If the association does not refer to another entity or that entity
  1252. // is not to be deleted, there is no ordering problem and we can
  1253. // skip this particular association.
  1254. if ($targetEntity === null || ! $sort->hasNode($targetEntity)) {
  1255. continue;
  1256. }
  1257. // Get the entities representing the SCC
  1258. $targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
  1259. // When we have a dependency between two different groups of strongly connected nodes,
  1260. // add it to the computation.
  1261. // The dependency direction implies that "$targetEntityComponent depends on $entityComponent
  1262. // being deleted first". The topological sort will output the depended-upon nodes first,
  1263. // so we can work through the result in the returned order.
  1264. if ($targetEntityComponent !== $entityComponent) {
  1265. $sort->addEdge($targetEntityComponent, $entityComponent, false);
  1266. }
  1267. }
  1268. }
  1269. return $sort->sort();
  1270. }
  1271. /**
  1272. * Schedules an entity for insertion into the database.
  1273. * If the entity already has an identifier, it will be added to the identity map.
  1274. *
  1275. * @param object $entity The entity to schedule for insertion.
  1276. *
  1277. * @return void
  1278. *
  1279. * @throws ORMInvalidArgumentException
  1280. * @throws InvalidArgumentException
  1281. */
  1282. public function scheduleForInsert($entity)
  1283. {
  1284. $oid = spl_object_id($entity);
  1285. if (isset($this->entityUpdates[$oid])) {
  1286. throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
  1287. }
  1288. if (isset($this->entityDeletions[$oid])) {
  1289. throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
  1290. }
  1291. if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
  1292. throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
  1293. }
  1294. if (isset($this->entityInsertions[$oid])) {
  1295. throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
  1296. }
  1297. $this->entityInsertions[$oid] = $entity;
  1298. if (isset($this->entityIdentifiers[$oid])) {
  1299. $this->addToIdentityMap($entity);
  1300. }
  1301. if ($entity instanceof NotifyPropertyChanged) {
  1302. $entity->addPropertyChangedListener($this);
  1303. }
  1304. }
  1305. /**
  1306. * Checks whether an entity is scheduled for insertion.
  1307. *
  1308. * @param object $entity
  1309. *
  1310. * @return bool
  1311. */
  1312. public function isScheduledForInsert($entity)
  1313. {
  1314. return isset($this->entityInsertions[spl_object_id($entity)]);
  1315. }
  1316. /**
  1317. * Schedules an entity for being updated.
  1318. *
  1319. * @param object $entity The entity to schedule for being updated.
  1320. *
  1321. * @return void
  1322. *
  1323. * @throws ORMInvalidArgumentException
  1324. */
  1325. public function scheduleForUpdate($entity)
  1326. {
  1327. $oid = spl_object_id($entity);
  1328. if (! isset($this->entityIdentifiers[$oid])) {
  1329. throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
  1330. }
  1331. if (isset($this->entityDeletions[$oid])) {
  1332. throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
  1333. }
  1334. if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
  1335. $this->entityUpdates[$oid] = $entity;
  1336. }
  1337. }
  1338. /**
  1339. * INTERNAL:
  1340. * Schedules an extra update that will be executed immediately after the
  1341. * regular entity updates within the currently running commit cycle.
  1342. *
  1343. * Extra updates for entities are stored as (entity, changeset) tuples.
  1344. *
  1345. * @param object $entity The entity for which to schedule an extra update.
  1346. * @phpstan-param array<string, array{mixed, mixed}> $changeset The changeset of the entity (what to update).
  1347. *
  1348. * @return void
  1349. *
  1350. * @ignore
  1351. */
  1352. public function scheduleExtraUpdate($entity, array $changeset)
  1353. {
  1354. $oid = spl_object_id($entity);
  1355. $extraUpdate = [$entity, $changeset];
  1356. if (isset($this->extraUpdates[$oid])) {
  1357. [, $changeset2] = $this->extraUpdates[$oid];
  1358. $extraUpdate = [$entity, $changeset + $changeset2];
  1359. }
  1360. $this->extraUpdates[$oid] = $extraUpdate;
  1361. }
  1362. /**
  1363. * Checks whether an entity is registered as dirty in the unit of work.
  1364. * Note: Is not very useful currently as dirty entities are only registered
  1365. * at commit time.
  1366. *
  1367. * @param object $entity
  1368. *
  1369. * @return bool
  1370. */
  1371. public function isScheduledForUpdate($entity)
  1372. {
  1373. return isset($this->entityUpdates[spl_object_id($entity)]);
  1374. }
  1375. /**
  1376. * Checks whether an entity is registered to be checked in the unit of work.
  1377. *
  1378. * @param object $entity
  1379. *
  1380. * @return bool
  1381. */
  1382. public function isScheduledForDirtyCheck($entity)
  1383. {
  1384. $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
  1385. return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
  1386. }
  1387. /**
  1388. * INTERNAL:
  1389. * Schedules an entity for deletion.
  1390. *
  1391. * @param object $entity
  1392. *
  1393. * @return void
  1394. */
  1395. public function scheduleForDelete($entity)
  1396. {
  1397. $oid = spl_object_id($entity);
  1398. if (isset($this->entityInsertions[$oid])) {
  1399. if ($this->isInIdentityMap($entity)) {
  1400. $this->removeFromIdentityMap($entity);
  1401. }
  1402. unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
  1403. return; // entity has not been persisted yet, so nothing more to do.
  1404. }
  1405. if (! $this->isInIdentityMap($entity)) {
  1406. return;
  1407. }
  1408. unset($this->entityUpdates[$oid]);
  1409. if (! isset($this->entityDeletions[$oid])) {
  1410. $this->entityDeletions[$oid] = $entity;
  1411. $this->entityStates[$oid] = self::STATE_REMOVED;
  1412. }
  1413. }
  1414. /**
  1415. * Checks whether an entity is registered as removed/deleted with the unit
  1416. * of work.
  1417. *
  1418. * @param object $entity
  1419. *
  1420. * @return bool
  1421. */
  1422. public function isScheduledForDelete($entity)
  1423. {
  1424. return isset($this->entityDeletions[spl_object_id($entity)]);
  1425. }
  1426. /**
  1427. * Checks whether an entity is scheduled for insertion, update or deletion.
  1428. *
  1429. * @param object $entity
  1430. *
  1431. * @return bool
  1432. */
  1433. public function isEntityScheduled($entity)
  1434. {
  1435. $oid = spl_object_id($entity);
  1436. return isset($this->entityInsertions[$oid])
  1437. || isset($this->entityUpdates[$oid])
  1438. || isset($this->entityDeletions[$oid]);
  1439. }
  1440. /**
  1441. * INTERNAL:
  1442. * Registers an entity in the identity map.
  1443. * Note that entities in a hierarchy are registered with the class name of
  1444. * the root entity.
  1445. *
  1446. * @param object $entity The entity to register.
  1447. *
  1448. * @return bool TRUE if the registration was successful, FALSE if the identity of
  1449. * the entity in question is already managed.
  1450. *
  1451. * @throws ORMInvalidArgumentException
  1452. * @throws EntityIdentityCollisionException
  1453. *
  1454. * @ignore
  1455. */
  1456. public function addToIdentityMap($entity)
  1457. {
  1458. $classMetadata = $this->em->getClassMetadata(get_class($entity));
  1459. $idHash = $this->getIdHashByEntity($entity);
  1460. $className = $classMetadata->rootEntityName;
  1461. if (isset($this->identityMap[$className][$idHash])) {
  1462. if ($this->identityMap[$className][$idHash] !== $entity) {
  1463. if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
  1464. throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash);
  1465. }
  1466. Deprecation::trigger(
  1467. 'doctrine/orm',
  1468. 'https://github.com/doctrine/orm/pull/10785',
  1469. <<<'EXCEPTION'
  1470. While adding an entity of class %s with an ID hash of "%s" to the identity map,
  1471. another object of class %s was already present for the same ID. This will trigger
  1472. an exception in ORM 3.0.
  1473. IDs should uniquely map to entity object instances. This problem may occur if:
  1474. - you use application-provided IDs and reuse ID values;
  1475. - database-provided IDs are reassigned after truncating the database without
  1476. clearing the EntityManager;
  1477. - you might have been using EntityManager#getReference() to create a reference
  1478. for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
  1479. entity.
  1480. Otherwise, it might be an ORM-internal inconsistency, please report it.
  1481. To opt-in to the new exception, call
  1482. \Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
  1483. manager's configuration.
  1484. EXCEPTION
  1485. ,
  1486. get_class($entity),
  1487. $idHash,
  1488. get_class($this->identityMap[$className][$idHash])
  1489. );
  1490. }
  1491. return false;
  1492. }
  1493. $this->identityMap[$className][$idHash] = $entity;
  1494. return true;
  1495. }
  1496. /**
  1497. * Gets the id hash of an entity by its identifier.
  1498. *
  1499. * @param array<string|int, mixed> $identifier The identifier of an entity
  1500. *
  1501. * @return string The entity id hash.
  1502. */
  1503. final public static function getIdHashByIdentifier(array $identifier): string
  1504. {
  1505. foreach ($identifier as $k => $value) {
  1506. if ($value instanceof BackedEnum) {
  1507. $identifier[$k] = $value->value;
  1508. }
  1509. }
  1510. return implode(
  1511. ' ',
  1512. $identifier
  1513. );
  1514. }
  1515. /**
  1516. * Gets the id hash of an entity.
  1517. *
  1518. * @param object $entity The entity managed by Unit Of Work
  1519. *
  1520. * @return string The entity id hash.
  1521. */
  1522. public function getIdHashByEntity($entity): string
  1523. {
  1524. $identifier = $this->entityIdentifiers[spl_object_id($entity)];
  1525. if (empty($identifier) || in_array(null, $identifier, true)) {
  1526. $classMetadata = $this->em->getClassMetadata(get_class($entity));
  1527. throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
  1528. }
  1529. return self::getIdHashByIdentifier($identifier);
  1530. }
  1531. /**
  1532. * Gets the state of an entity with regard to the current unit of work.
  1533. *
  1534. * @param object $entity
  1535. * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
  1536. * This parameter can be set to improve performance of entity state detection
  1537. * by potentially avoiding a database lookup if the distinction between NEW and DETACHED
  1538. * is either known or does not matter for the caller of the method.
  1539. * @phpstan-param self::STATE_*|null $assume
  1540. *
  1541. * @return int The entity state.
  1542. * @phpstan-return self::STATE_*
  1543. */
  1544. public function getEntityState($entity, $assume = null)
  1545. {
  1546. $oid = spl_object_id($entity);
  1547. if (isset($this->entityStates[$oid])) {
  1548. return $this->entityStates[$oid];
  1549. }
  1550. if ($assume !== null) {
  1551. return $assume;
  1552. }
  1553. // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
  1554. // Note that you can not remember the NEW or DETACHED state in _entityStates since
  1555. // the UoW does not hold references to such objects and the object hash can be reused.
  1556. // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
  1557. $class = $this->em->getClassMetadata(get_class($entity));
  1558. $id = $class->getIdentifierValues($entity);
  1559. if (! $id) {
  1560. return self::STATE_NEW;
  1561. }
  1562. if ($class->containsForeignIdentifier || $class->containsEnumIdentifier) {
  1563. $id = $this->identifierFlattener->flattenIdentifier($class, $id);
  1564. }
  1565. switch (true) {
  1566. case $class->isIdentifierNatural():
  1567. // Check for a version field, if available, to avoid a db lookup.
  1568. if ($class->isVersioned) {
  1569. assert($class->versionField !== null);
  1570. return $class->getFieldValue($entity, $class->versionField)
  1571. ? self::STATE_DETACHED
  1572. : self::STATE_NEW;
  1573. }
  1574. // Last try before db lookup: check the identity map.
  1575. if ($this->tryGetById($id, $class->rootEntityName)) {
  1576. return self::STATE_DETACHED;
  1577. }
  1578. // db lookup
  1579. if ($this->getEntityPersister($class->name)->exists($entity)) {
  1580. return self::STATE_DETACHED;
  1581. }
  1582. return self::STATE_NEW;
  1583. case ! $class->idGenerator->isPostInsertGenerator():
  1584. // if we have a pre insert generator we can't be sure that having an id
  1585. // really means that the entity exists. We have to verify this through
  1586. // the last resort: a db lookup
  1587. // Last try before db lookup: check the identity map.
  1588. if ($this->tryGetById($id, $class->rootEntityName)) {
  1589. return self::STATE_DETACHED;
  1590. }
  1591. // db lookup
  1592. if ($this->getEntityPersister($class->name)->exists($entity)) {
  1593. return self::STATE_DETACHED;
  1594. }
  1595. return self::STATE_NEW;
  1596. default:
  1597. return self::STATE_DETACHED;
  1598. }
  1599. }
  1600. /**
  1601. * INTERNAL:
  1602. * Removes an entity from the identity map. This effectively detaches the
  1603. * entity from the persistence management of Doctrine.
  1604. *
  1605. * @param object $entity
  1606. *
  1607. * @return bool
  1608. *
  1609. * @throws ORMInvalidArgumentException
  1610. *
  1611. * @ignore
  1612. */
  1613. public function removeFromIdentityMap($entity)
  1614. {
  1615. $oid = spl_object_id($entity);
  1616. $classMetadata = $this->em->getClassMetadata(get_class($entity));
  1617. $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
  1618. if ($idHash === '') {
  1619. throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
  1620. }
  1621. $className = $classMetadata->rootEntityName;
  1622. if (isset($this->identityMap[$className][$idHash])) {
  1623. unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
  1624. //$this->entityStates[$oid] = self::STATE_DETACHED;
  1625. return true;
  1626. }
  1627. return false;
  1628. }
  1629. /**
  1630. * INTERNAL:
  1631. * Gets an entity in the identity map by its identifier hash.
  1632. *
  1633. * @param string $idHash
  1634. * @param string $rootClassName
  1635. *
  1636. * @return object
  1637. *
  1638. * @ignore
  1639. */
  1640. public function getByIdHash($idHash, $rootClassName)
  1641. {
  1642. return $this->identityMap[$rootClassName][$idHash];
  1643. }
  1644. /**
  1645. * INTERNAL:
  1646. * Tries to get an entity by its identifier hash. If no entity is found for
  1647. * the given hash, FALSE is returned.
  1648. *
  1649. * @param mixed $idHash (must be possible to cast it to string)
  1650. * @param string $rootClassName
  1651. *
  1652. * @return false|object The found entity or FALSE.
  1653. *
  1654. * @ignore
  1655. */
  1656. public function tryGetByIdHash($idHash, $rootClassName)
  1657. {
  1658. $stringIdHash = (string) $idHash;
  1659. return $this->identityMap[$rootClassName][$stringIdHash] ?? false;
  1660. }
  1661. /**
  1662. * Checks whether an entity is registered in the identity map of this UnitOfWork.
  1663. *
  1664. * @param object $entity
  1665. *
  1666. * @return bool
  1667. */
  1668. public function isInIdentityMap($entity)
  1669. {
  1670. $oid = spl_object_id($entity);
  1671. if (empty($this->entityIdentifiers[$oid])) {
  1672. return false;
  1673. }
  1674. $classMetadata = $this->em->getClassMetadata(get_class($entity));
  1675. $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
  1676. return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
  1677. }
  1678. /**
  1679. * INTERNAL:
  1680. * Checks whether an identifier hash exists in the identity map.
  1681. *
  1682. * @param string $idHash
  1683. * @param string $rootClassName
  1684. *
  1685. * @return bool
  1686. *
  1687. * @ignore
  1688. */
  1689. public function containsIdHash($idHash, $rootClassName)
  1690. {
  1691. return isset($this->identityMap[$rootClassName][$idHash]);
  1692. }
  1693. /**
  1694. * Persists an entity as part of the current unit of work.
  1695. *
  1696. * @param object $entity The entity to persist.
  1697. *
  1698. * @return void
  1699. */
  1700. public function persist($entity)
  1701. {
  1702. $visited = [];
  1703. $this->doPersist($entity, $visited);
  1704. }
  1705. /**
  1706. * Persists an entity as part of the current unit of work.
  1707. *
  1708. * This method is internally called during persist() cascades as it tracks
  1709. * the already visited entities to prevent infinite recursions.
  1710. *
  1711. * @param object $entity The entity to persist.
  1712. * @phpstan-param array<int, object> $visited The already visited entities.
  1713. *
  1714. * @throws ORMInvalidArgumentException
  1715. * @throws UnexpectedValueException
  1716. */
  1717. private function doPersist($entity, array &$visited): void
  1718. {
  1719. $oid = spl_object_id($entity);
  1720. if (isset($visited[$oid])) {
  1721. return; // Prevent infinite recursion
  1722. }
  1723. $visited[$oid] = $entity; // Mark visited
  1724. $class = $this->em->getClassMetadata(get_class($entity));
  1725. // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
  1726. // If we would detect DETACHED here we would throw an exception anyway with the same
  1727. // consequences (not recoverable/programming error), so just assuming NEW here
  1728. // lets us avoid some database lookups for entities with natural identifiers.
  1729. $entityState = $this->getEntityState($entity, self::STATE_NEW);
  1730. switch ($entityState) {
  1731. case self::STATE_MANAGED:
  1732. // Nothing to do, except if policy is "deferred explicit"
  1733. if ($class->isChangeTrackingDeferredExplicit()) {
  1734. $this->scheduleForDirtyCheck($entity);
  1735. }
  1736. break;
  1737. case self::STATE_NEW:
  1738. $this->persistNew($class, $entity);
  1739. break;
  1740. case self::STATE_REMOVED:
  1741. // Entity becomes managed again
  1742. unset($this->entityDeletions[$oid]);
  1743. $this->addToIdentityMap($entity);
  1744. $this->entityStates[$oid] = self::STATE_MANAGED;
  1745. if ($class->isChangeTrackingDeferredExplicit()) {
  1746. $this->scheduleForDirtyCheck($entity);
  1747. }
  1748. break;
  1749. case self::STATE_DETACHED:
  1750. // Can actually not happen right now since we assume STATE_NEW.
  1751. throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
  1752. default:
  1753. throw new UnexpectedValueException(sprintf(
  1754. 'Unexpected entity state: %s. %s',
  1755. $entityState,
  1756. self::objToStr($entity)
  1757. ));
  1758. }
  1759. $this->cascadePersist($entity, $visited);
  1760. }
  1761. /**
  1762. * Deletes an entity as part of the current unit of work.
  1763. *
  1764. * @param object $entity The entity to remove.
  1765. *
  1766. * @return void
  1767. */
  1768. public function remove($entity)
  1769. {
  1770. $visited = [];
  1771. $this->doRemove($entity, $visited);
  1772. }
  1773. /**
  1774. * Deletes an entity as part of the current unit of work.
  1775. *
  1776. * This method is internally called during delete() cascades as it tracks
  1777. * the already visited entities to prevent infinite recursions.
  1778. *
  1779. * @param object $entity The entity to delete.
  1780. * @phpstan-param array<int, object> $visited The map of the already visited entities.
  1781. *
  1782. * @throws ORMInvalidArgumentException If the instance is a detached entity.
  1783. * @throws UnexpectedValueException
  1784. */
  1785. private function doRemove($entity, array &$visited): void
  1786. {
  1787. $oid = spl_object_id($entity);
  1788. if (isset($visited[$oid])) {
  1789. return; // Prevent infinite recursion
  1790. }
  1791. $visited[$oid] = $entity; // mark visited
  1792. // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
  1793. // can cause problems when a lazy proxy has to be initialized for the cascade operation.
  1794. $this->cascadeRemove($entity, $visited);
  1795. $class = $this->em->getClassMetadata(get_class($entity));
  1796. $entityState = $this->getEntityState($entity);
  1797. switch ($entityState) {
  1798. case self::STATE_NEW:
  1799. case self::STATE_REMOVED:
  1800. // nothing to do
  1801. break;
  1802. case self::STATE_MANAGED:
  1803. $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
  1804. if ($invoke !== ListenersInvoker::INVOKE_NONE) {
  1805. $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new PreRemoveEventArgs($entity, $this->em), $invoke);
  1806. }
  1807. $this->scheduleForDelete($entity);
  1808. break;
  1809. case self::STATE_DETACHED:
  1810. throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
  1811. default:
  1812. throw new UnexpectedValueException(sprintf(
  1813. 'Unexpected entity state: %s. %s',
  1814. $entityState,
  1815. self::objToStr($entity)
  1816. ));
  1817. }
  1818. }
  1819. /**
  1820. * Merges the state of the given detached entity into this UnitOfWork.
  1821. *
  1822. * @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
  1823. *
  1824. * @param object $entity
  1825. *
  1826. * @return object The managed copy of the entity.
  1827. *
  1828. * @throws OptimisticLockException If the entity uses optimistic locking through a version
  1829. * attribute and the version check against the managed copy fails.
  1830. */
  1831. public function merge($entity)
  1832. {
  1833. $visited = [];
  1834. return $this->doMerge($entity, $visited);
  1835. }
  1836. /**
  1837. * Executes a merge operation on an entity.
  1838. *
  1839. * @param object $entity
  1840. * @phpstan-param AssociationMapping|null $assoc
  1841. * @phpstan-param array<int, object> $visited
  1842. *
  1843. * @return object The managed copy of the entity.
  1844. *
  1845. * @throws OptimisticLockException If the entity uses optimistic locking through a version
  1846. * attribute and the version check against the managed copy fails.
  1847. * @throws ORMInvalidArgumentException If the entity instance is NEW.
  1848. * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided.
  1849. */
  1850. private function doMerge(
  1851. $entity,
  1852. array &$visited,
  1853. $prevManagedCopy = null,
  1854. ?array $assoc = null
  1855. ) {
  1856. $oid = spl_object_id($entity);
  1857. if (isset($visited[$oid])) {
  1858. $managedCopy = $visited[$oid];
  1859. if ($prevManagedCopy !== null) {
  1860. $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
  1861. }
  1862. return $managedCopy;
  1863. }
  1864. $class = $this->em->getClassMetadata(get_class($entity));
  1865. // First we assume DETACHED, although it can still be NEW but we can avoid
  1866. // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
  1867. // we need to fetch it from the db anyway in order to merge.
  1868. // MANAGED entities are ignored by the merge operation.
  1869. $managedCopy = $entity;
  1870. if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
  1871. // Try to look the entity up in the identity map.
  1872. $id = $class->getIdentifierValues($entity);
  1873. // If there is no ID, it is actually NEW.
  1874. if (! $id) {
  1875. $managedCopy = $this->newInstance($class);
  1876. $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
  1877. $this->persistNew($class, $managedCopy);
  1878. } else {
  1879. $flatId = $class->containsForeignIdentifier || $class->containsEnumIdentifier
  1880. ? $this->identifierFlattener->flattenIdentifier($class, $id)
  1881. : $id;
  1882. $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
  1883. if ($managedCopy) {
  1884. // We have the entity in-memory already, just make sure its not removed.
  1885. if ($this->getEntityState($managedCopy) === self::STATE_REMOVED) {
  1886. throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, 'merge');
  1887. }
  1888. } else {
  1889. // We need to fetch the managed copy in order to merge.
  1890. $managedCopy = $this->em->find($class->name, $flatId);
  1891. }
  1892. if ($managedCopy === null) {
  1893. // If the identifier is ASSIGNED, it is NEW, otherwise an error
  1894. // since the managed entity was not found.
  1895. if (! $class->isIdentifierNatural()) {
  1896. throw EntityNotFoundException::fromClassNameAndIdentifier(
  1897. $class->getName(),
  1898. $this->identifierFlattener->flattenIdentifier($class, $id)
  1899. );
  1900. }
  1901. $managedCopy = $this->newInstance($class);
  1902. $class->setIdentifierValues($managedCopy, $id);
  1903. $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
  1904. $this->persistNew($class, $managedCopy);
  1905. } else {
  1906. $this->ensureVersionMatch($class, $entity, $managedCopy);
  1907. $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
  1908. }
  1909. }
  1910. $visited[$oid] = $managedCopy; // mark visited
  1911. if ($class->isChangeTrackingDeferredExplicit()) {
  1912. $this->scheduleForDirtyCheck($entity);
  1913. }
  1914. }
  1915. if ($prevManagedCopy !== null) {
  1916. $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
  1917. }
  1918. // Mark the managed copy visited as well
  1919. $visited[spl_object_id($managedCopy)] = $managedCopy;
  1920. $this->cascadeMerge($entity, $managedCopy, $visited);
  1921. return $managedCopy;
  1922. }
  1923. /**
  1924. * @param object $entity
  1925. * @param object $managedCopy
  1926. * @phpstan-param ClassMetadata<T> $class
  1927. * @phpstan-param T $entity
  1928. * @phpstan-param T $managedCopy
  1929. *
  1930. * @throws OptimisticLockException
  1931. *
  1932. * @template T of object
  1933. */
  1934. private function ensureVersionMatch(
  1935. ClassMetadata $class,
  1936. $entity,
  1937. $managedCopy
  1938. ): void {
  1939. if (! ($class->isVersioned && ! $this->isUninitializedObject($managedCopy) && ! $this->isUninitializedObject($entity))) {
  1940. return;
  1941. }
  1942. assert($class->versionField !== null);
  1943. $reflField = $class->reflFields[$class->versionField];
  1944. $managedCopyVersion = $reflField->getValue($managedCopy);
  1945. $entityVersion = $reflField->getValue($entity);
  1946. // Throw exception if versions don't match.
  1947. // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
  1948. if ($managedCopyVersion == $entityVersion) {
  1949. return;
  1950. }
  1951. throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
  1952. }
  1953. /**
  1954. * Sets/adds associated managed copies into the previous entity's association field
  1955. *
  1956. * @param object $entity
  1957. * @phpstan-param AssociationMapping $association
  1958. */
  1959. private function updateAssociationWithMergedEntity(
  1960. $entity,
  1961. array $association,
  1962. $previousManagedCopy,
  1963. $managedCopy
  1964. ): void {
  1965. $assocField = $association['fieldName'];
  1966. $prevClass = $this->em->getClassMetadata(get_class($previousManagedCopy));
  1967. if ($association['type'] & ClassMetadata::TO_ONE) {
  1968. $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
  1969. return;
  1970. }
  1971. $value = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
  1972. $value[] = $managedCopy;
  1973. if ($association['type'] === ClassMetadata::ONE_TO_MANY) {
  1974. $class = $this->em->getClassMetadata(get_class($entity));
  1975. $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
  1976. }
  1977. }
  1978. /**
  1979. * Detaches an entity from the persistence management. It's persistence will
  1980. * no longer be managed by Doctrine.
  1981. *
  1982. * @param object $entity The entity to detach.
  1983. *
  1984. * @return void
  1985. */
  1986. public function detach($entity)
  1987. {
  1988. $visited = [];
  1989. $this->doDetach($entity, $visited);
  1990. }
  1991. /**
  1992. * Executes a detach operation on the given entity.
  1993. *
  1994. * @param object $entity
  1995. * @param mixed[] $visited
  1996. * @param bool $noCascade if true, don't cascade detach operation.
  1997. */
  1998. private function doDetach(
  1999. $entity,
  2000. array &$visited,
  2001. bool $noCascade = false
  2002. ): void {
  2003. $oid = spl_object_id($entity);
  2004. if (isset($visited[$oid])) {
  2005. return; // Prevent infinite recursion
  2006. }
  2007. $visited[$oid] = $entity; // mark visited
  2008. switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
  2009. case self::STATE_MANAGED:
  2010. if ($this->isInIdentityMap($entity)) {
  2011. $this->removeFromIdentityMap($entity);
  2012. }
  2013. unset(
  2014. $this->entityInsertions[$oid],
  2015. $this->entityUpdates[$oid],
  2016. $this->entityDeletions[$oid],
  2017. $this->entityIdentifiers[$oid],
  2018. $this->entityStates[$oid],
  2019. $this->originalEntityData[$oid]
  2020. );
  2021. break;
  2022. case self::STATE_NEW:
  2023. case self::STATE_DETACHED:
  2024. return;
  2025. }
  2026. if (! $noCascade) {
  2027. $this->cascadeDetach($entity, $visited);
  2028. }
  2029. }
  2030. /**
  2031. * Refreshes the state of the given entity from the database, overwriting
  2032. * any local, unpersisted changes.
  2033. *
  2034. * @param object $entity The entity to refresh
  2035. *
  2036. * @return void
  2037. *
  2038. * @throws InvalidArgumentException If the entity is not MANAGED.
  2039. * @throws TransactionRequiredException
  2040. */
  2041. public function refresh($entity)
  2042. {
  2043. $visited = [];
  2044. $lockMode = null;
  2045. if (func_num_args() > 1) {
  2046. $lockMode = func_get_arg(1);
  2047. }
  2048. $this->doRefresh($entity, $visited, $lockMode);
  2049. }
  2050. /**
  2051. * Executes a refresh operation on an entity.
  2052. *
  2053. * @param object $entity The entity to refresh.
  2054. * @phpstan-param array<int, object> $visited The already visited entities during cascades.
  2055. * @phpstan-param LockMode::*|null $lockMode
  2056. *
  2057. * @throws ORMInvalidArgumentException If the entity is not MANAGED.
  2058. * @throws TransactionRequiredException
  2059. */
  2060. private function doRefresh($entity, array &$visited, ?int $lockMode = null): void
  2061. {
  2062. switch (true) {
  2063. case $lockMode === LockMode::PESSIMISTIC_READ:
  2064. case $lockMode === LockMode::PESSIMISTIC_WRITE:
  2065. if (! $this->em->getConnection()->isTransactionActive()) {
  2066. throw TransactionRequiredException::transactionRequired();
  2067. }
  2068. }
  2069. $oid = spl_object_id($entity);
  2070. if (isset($visited[$oid])) {
  2071. return; // Prevent infinite recursion
  2072. }
  2073. $visited[$oid] = $entity; // mark visited
  2074. $class = $this->em->getClassMetadata(get_class($entity));
  2075. if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
  2076. throw ORMInvalidArgumentException::entityNotManaged($entity);
  2077. }
  2078. $this->cascadeRefresh($entity, $visited, $lockMode);
  2079. $this->getEntityPersister($class->name)->refresh(
  2080. array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
  2081. $entity,
  2082. $lockMode
  2083. );
  2084. }
  2085. /**
  2086. * Cascades a refresh operation to associated entities.
  2087. *
  2088. * @param object $entity
  2089. * @phpstan-param array<int, object> $visited
  2090. * @phpstan-param LockMode::*|null $lockMode
  2091. */
  2092. private function cascadeRefresh($entity, array &$visited, ?int $lockMode = null): void
  2093. {
  2094. $class = $this->em->getClassMetadata(get_class($entity));
  2095. $associationMappings = array_filter(
  2096. $class->associationMappings,
  2097. static function ($assoc) {
  2098. return $assoc['isCascadeRefresh'];
  2099. }
  2100. );
  2101. foreach ($associationMappings as $assoc) {
  2102. $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
  2103. switch (true) {
  2104. case $relatedEntities instanceof PersistentCollection:
  2105. // Unwrap so that foreach() does not initialize
  2106. $relatedEntities = $relatedEntities->unwrap();
  2107. // break; is commented intentionally!
  2108. case $relatedEntities instanceof Collection:
  2109. case is_array($relatedEntities):
  2110. foreach ($relatedEntities as $relatedEntity) {
  2111. $this->doRefresh($relatedEntity, $visited, $lockMode);
  2112. }
  2113. break;
  2114. case $relatedEntities !== null:
  2115. $this->doRefresh($relatedEntities, $visited, $lockMode);
  2116. break;
  2117. default:
  2118. // Do nothing
  2119. }
  2120. }
  2121. }
  2122. /**
  2123. * Cascades a detach operation to associated entities.
  2124. *
  2125. * @param object $entity
  2126. * @param array<int, object> $visited
  2127. */
  2128. private function cascadeDetach($entity, array &$visited): void
  2129. {
  2130. $class = $this->em->getClassMetadata(get_class($entity));
  2131. $associationMappings = array_filter(
  2132. $class->associationMappings,
  2133. static function ($assoc) {
  2134. return $assoc['isCascadeDetach'];
  2135. }
  2136. );
  2137. foreach ($associationMappings as $assoc) {
  2138. $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
  2139. switch (true) {
  2140. case $relatedEntities instanceof PersistentCollection:
  2141. // Unwrap so that foreach() does not initialize
  2142. $relatedEntities = $relatedEntities->unwrap();
  2143. // break; is commented intentionally!
  2144. case $relatedEntities instanceof Collection:
  2145. case is_array($relatedEntities):
  2146. foreach ($relatedEntities as $relatedEntity) {
  2147. $this->doDetach($relatedEntity, $visited);
  2148. }
  2149. break;
  2150. case $relatedEntities !== null:
  2151. $this->doDetach($relatedEntities, $visited);
  2152. break;
  2153. default:
  2154. // Do nothing
  2155. }
  2156. }
  2157. }
  2158. /**
  2159. * Cascades a merge operation to associated entities.
  2160. *
  2161. * @param object $entity
  2162. * @param object $managedCopy
  2163. * @phpstan-param array<int, object> $visited
  2164. */
  2165. private function cascadeMerge($entity, $managedCopy, array &$visited): void
  2166. {
  2167. $class = $this->em->getClassMetadata(get_class($entity));
  2168. $associationMappings = array_filter(
  2169. $class->associationMappings,
  2170. static function ($assoc) {
  2171. return $assoc['isCascadeMerge'];
  2172. }
  2173. );
  2174. foreach ($associationMappings as $assoc) {
  2175. $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
  2176. if ($relatedEntities instanceof Collection) {
  2177. if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
  2178. continue;
  2179. }
  2180. if ($relatedEntities instanceof PersistentCollection) {
  2181. // Unwrap so that foreach() does not initialize
  2182. $relatedEntities = $relatedEntities->unwrap();
  2183. }
  2184. foreach ($relatedEntities as $relatedEntity) {
  2185. $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
  2186. }
  2187. } elseif ($relatedEntities !== null) {
  2188. $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
  2189. }
  2190. }
  2191. }
  2192. /**
  2193. * Cascades the save operation to associated entities.
  2194. *
  2195. * @param object $entity
  2196. * @phpstan-param array<int, object> $visited
  2197. */
  2198. private function cascadePersist($entity, array &$visited): void
  2199. {
  2200. if ($this->isUninitializedObject($entity)) {
  2201. // nothing to do - proxy is not initialized, therefore we don't do anything with it
  2202. return;
  2203. }
  2204. $class = $this->em->getClassMetadata(get_class($entity));
  2205. $associationMappings = array_filter(
  2206. $class->associationMappings,
  2207. static function ($assoc) {
  2208. return $assoc['isCascadePersist'];
  2209. }
  2210. );
  2211. foreach ($associationMappings as $assoc) {
  2212. $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
  2213. switch (true) {
  2214. case $relatedEntities instanceof PersistentCollection:
  2215. // Unwrap so that foreach() does not initialize
  2216. $relatedEntities = $relatedEntities->unwrap();
  2217. // break; is commented intentionally!
  2218. case $relatedEntities instanceof Collection:
  2219. case is_array($relatedEntities):
  2220. if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
  2221. throw ORMInvalidArgumentException::invalidAssociation(
  2222. $this->em->getClassMetadata($assoc['targetEntity']),
  2223. $assoc,
  2224. $relatedEntities
  2225. );
  2226. }
  2227. foreach ($relatedEntities as $relatedEntity) {
  2228. $this->doPersist($relatedEntity, $visited);
  2229. }
  2230. break;
  2231. case $relatedEntities !== null:
  2232. if (! $relatedEntities instanceof $assoc['targetEntity']) {
  2233. throw ORMInvalidArgumentException::invalidAssociation(
  2234. $this->em->getClassMetadata($assoc['targetEntity']),
  2235. $assoc,
  2236. $relatedEntities
  2237. );
  2238. }
  2239. $this->doPersist($relatedEntities, $visited);
  2240. break;
  2241. default:
  2242. // Do nothing
  2243. }
  2244. }
  2245. }
  2246. /**
  2247. * Cascades the delete operation to associated entities.
  2248. *
  2249. * @param object $entity
  2250. * @phpstan-param array<int, object> $visited
  2251. */
  2252. private function cascadeRemove($entity, array &$visited): void
  2253. {
  2254. $class = $this->em->getClassMetadata(get_class($entity));
  2255. $associationMappings = array_filter(
  2256. $class->associationMappings,
  2257. static function ($assoc) {
  2258. return $assoc['isCascadeRemove'];
  2259. }
  2260. );
  2261. if ($associationMappings) {
  2262. $this->initializeObject($entity);
  2263. }
  2264. $entitiesToCascade = [];
  2265. foreach ($associationMappings as $assoc) {
  2266. $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
  2267. switch (true) {
  2268. case $relatedEntities instanceof Collection:
  2269. case is_array($relatedEntities):
  2270. // If its a PersistentCollection initialization is intended! No unwrap!
  2271. foreach ($relatedEntities as $relatedEntity) {
  2272. $entitiesToCascade[] = $relatedEntity;
  2273. }
  2274. break;
  2275. case $relatedEntities !== null:
  2276. $entitiesToCascade[] = $relatedEntities;
  2277. break;
  2278. default:
  2279. // Do nothing
  2280. }
  2281. }
  2282. foreach ($entitiesToCascade as $relatedEntity) {
  2283. $this->doRemove($relatedEntity, $visited);
  2284. }
  2285. }
  2286. /**
  2287. * Acquire a lock on the given entity.
  2288. *
  2289. * @param object $entity
  2290. * @param int|DateTimeInterface|null $lockVersion
  2291. * @phpstan-param LockMode::* $lockMode
  2292. *
  2293. * @throws ORMInvalidArgumentException
  2294. * @throws TransactionRequiredException
  2295. * @throws OptimisticLockException
  2296. */
  2297. public function lock($entity, int $lockMode, $lockVersion = null): void
  2298. {
  2299. if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
  2300. throw ORMInvalidArgumentException::entityNotManaged($entity);
  2301. }
  2302. $class = $this->em->getClassMetadata(get_class($entity));
  2303. switch (true) {
  2304. case $lockMode === LockMode::OPTIMISTIC:
  2305. if (! $class->isVersioned) {
  2306. throw OptimisticLockException::notVersioned($class->name);
  2307. }
  2308. if ($lockVersion === null) {
  2309. return;
  2310. }
  2311. $this->initializeObject($entity);
  2312. assert($class->versionField !== null);
  2313. $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
  2314. // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
  2315. if ($entityVersion != $lockVersion) {
  2316. throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
  2317. }
  2318. break;
  2319. case $lockMode === LockMode::NONE:
  2320. case $lockMode === LockMode::PESSIMISTIC_READ:
  2321. case $lockMode === LockMode::PESSIMISTIC_WRITE:
  2322. if (! $this->em->getConnection()->isTransactionActive()) {
  2323. throw TransactionRequiredException::transactionRequired();
  2324. }
  2325. $oid = spl_object_id($entity);
  2326. $this->getEntityPersister($class->name)->lock(
  2327. array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
  2328. $lockMode
  2329. );
  2330. break;
  2331. default:
  2332. // Do nothing
  2333. }
  2334. }
  2335. /**
  2336. * Clears the UnitOfWork.
  2337. *
  2338. * @param string|null $entityName if given, only entities of this type will get detached.
  2339. *
  2340. * @return void
  2341. *
  2342. * @throws ORMInvalidArgumentException if an invalid entity name is given.
  2343. */
  2344. public function clear($entityName = null)
  2345. {
  2346. if ($entityName === null) {
  2347. $this->identityMap =
  2348. $this->entityIdentifiers =
  2349. $this->originalEntityData =
  2350. $this->entityChangeSets =
  2351. $this->entityStates =
  2352. $this->scheduledForSynchronization =
  2353. $this->entityInsertions =
  2354. $this->entityUpdates =
  2355. $this->entityDeletions =
  2356. $this->nonCascadedNewDetectedEntities =
  2357. $this->collectionDeletions =
  2358. $this->collectionUpdates =
  2359. $this->extraUpdates =
  2360. $this->readOnlyObjects =
  2361. $this->pendingCollectionElementRemovals =
  2362. $this->visitedCollections =
  2363. $this->eagerLoadingEntities =
  2364. $this->eagerLoadingCollections =
  2365. $this->orphanRemovals = [];
  2366. } else {
  2367. Deprecation::triggerIfCalledFromOutside(
  2368. 'doctrine/orm',
  2369. 'https://github.com/doctrine/orm/issues/8460',
  2370. 'Calling %s() with any arguments to clear specific entities is deprecated and will not be supported in Doctrine ORM 3.0.',
  2371. __METHOD__
  2372. );
  2373. $this->clearIdentityMapForEntityName($entityName);
  2374. $this->clearEntityInsertionsForEntityName($entityName);
  2375. }
  2376. if ($this->evm->hasListeners(Events::onClear)) {
  2377. $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
  2378. }
  2379. }
  2380. /**
  2381. * INTERNAL:
  2382. * Schedules an orphaned entity for removal. The remove() operation will be
  2383. * invoked on that entity at the beginning of the next commit of this
  2384. * UnitOfWork.
  2385. *
  2386. * @param object $entity
  2387. *
  2388. * @return void
  2389. *
  2390. * @ignore
  2391. */
  2392. public function scheduleOrphanRemoval($entity)
  2393. {
  2394. $this->orphanRemovals[spl_object_id($entity)] = $entity;
  2395. }
  2396. /**
  2397. * INTERNAL:
  2398. * Cancels a previously scheduled orphan removal.
  2399. *
  2400. * @param object $entity
  2401. *
  2402. * @return void
  2403. *
  2404. * @ignore
  2405. */
  2406. public function cancelOrphanRemoval($entity)
  2407. {
  2408. unset($this->orphanRemovals[spl_object_id($entity)]);
  2409. }
  2410. /**
  2411. * INTERNAL:
  2412. * Schedules a complete collection for removal when this UnitOfWork commits.
  2413. *
  2414. * @return void
  2415. */
  2416. public function scheduleCollectionDeletion(PersistentCollection $coll)
  2417. {
  2418. $coid = spl_object_id($coll);
  2419. // TODO: if $coll is already scheduled for recreation ... what to do?
  2420. // Just remove $coll from the scheduled recreations?
  2421. unset($this->collectionUpdates[$coid]);
  2422. $this->collectionDeletions[$coid] = $coll;
  2423. }
  2424. /** @return bool */
  2425. public function isCollectionScheduledForDeletion(PersistentCollection $coll)
  2426. {
  2427. return isset($this->collectionDeletions[spl_object_id($coll)]);
  2428. }
  2429. /** @return object */
  2430. private function newInstance(ClassMetadata $class)
  2431. {
  2432. $entity = $class->newInstance();
  2433. if ($entity instanceof ObjectManagerAware) {
  2434. $entity->injectObjectManager($this->em, $class);
  2435. }
  2436. return $entity;
  2437. }
  2438. /**
  2439. * INTERNAL:
  2440. * Creates an entity. Used for reconstitution of persistent entities.
  2441. *
  2442. * Internal note: Highly performance-sensitive method.
  2443. *
  2444. * @param class-string $className The name of the entity class.
  2445. * @param mixed[] $data The data for the entity.
  2446. * @param array<string, mixed> $hints Any hints to account for during reconstitution/lookup of the entity.
  2447. *
  2448. * @return object The managed entity instance.
  2449. *
  2450. * @ignore
  2451. * @todo Rename: getOrCreateEntity
  2452. */
  2453. public function createEntity($className, array $data, &$hints = [])
  2454. {
  2455. $class = $this->em->getClassMetadata($className);
  2456. $id = $this->identifierFlattener->flattenIdentifier($class, $data);
  2457. $idHash = self::getIdHashByIdentifier($id);
  2458. if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
  2459. $entity = $this->identityMap[$class->rootEntityName][$idHash];
  2460. $oid = spl_object_id($entity);
  2461. if (
  2462. isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])
  2463. ) {
  2464. $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
  2465. if (
  2466. $unmanagedProxy !== $entity
  2467. && $this->isIdentifierEquals($unmanagedProxy, $entity)
  2468. ) {
  2469. // We will hydrate the given un-managed proxy anyway:
  2470. // continue work, but consider it the entity from now on
  2471. $entity = $unmanagedProxy;
  2472. }
  2473. }
  2474. if ($this->isUninitializedObject($entity)) {
  2475. $entity->__setInitialized(true);
  2476. if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
  2477. // Initialize properties that have default values to their default value (similar to what
  2478. Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
  2479. }
  2480. } else {
  2481. if (
  2482. ! isset($hints[Query::HINT_REFRESH])
  2483. || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)
  2484. ) {
  2485. return $entity;
  2486. }
  2487. }
  2488. // inject ObjectManager upon refresh.
  2489. if ($entity instanceof ObjectManagerAware) {
  2490. $entity->injectObjectManager($this->em, $class);
  2491. }
  2492. $this->originalEntityData[$oid] = $data;
  2493. if ($entity instanceof NotifyPropertyChanged) {
  2494. $entity->addPropertyChangedListener($this);
  2495. }
  2496. } else {
  2497. $entity = $this->newInstance($class);
  2498. $oid = spl_object_id($entity);
  2499. $this->registerManaged($entity, $id, $data);
  2500. if (isset($hints[Query::HINT_READ_ONLY]) && $hints[Query::HINT_READ_ONLY] === true) {
  2501. $this->readOnlyObjects[$oid] = true;
  2502. }
  2503. }
  2504. foreach ($data as $field => $value) {
  2505. if (isset($class->fieldMappings[$field])) {
  2506. $class->reflFields[$field]->setValue($entity, $value);
  2507. }
  2508. }
  2509. // Loading the entity right here, if its in the eager loading map get rid of it there.
  2510. unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
  2511. if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && ! $this->eagerLoadingEntities[$class->rootEntityName]) {
  2512. unset($this->eagerLoadingEntities[$class->rootEntityName]);
  2513. }
  2514. // Properly initialize any unfetched associations, if partial objects are not allowed.
  2515. if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
  2516. Deprecation::trigger(
  2517. 'doctrine/orm',
  2518. 'https://github.com/doctrine/orm/issues/8471',
  2519. 'Partial Objects are deprecated (here entity %s)',
  2520. $className
  2521. );
  2522. return $entity;
  2523. }
  2524. foreach ($class->associationMappings as $field => $assoc) {
  2525. // Check if the association is not among the fetch-joined associations already.
  2526. if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
  2527. continue;
  2528. }
  2529. if (! isset($hints['fetchMode'][$class->name][$field])) {
  2530. $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
  2531. }
  2532. $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
  2533. switch (true) {
  2534. case $assoc['type'] & ClassMetadata::TO_ONE:
  2535. if (! $assoc['isOwningSide']) {
  2536. // use the given entity association
  2537. if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
  2538. $this->originalEntityData[$oid][$field] = $data[$field];
  2539. $class->reflFields[$field]->setValue($entity, $data[$field]);
  2540. $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
  2541. continue 2;
  2542. }
  2543. // Inverse side of x-to-one can never be lazy
  2544. $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
  2545. continue 2;
  2546. }
  2547. // use the entity association
  2548. if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
  2549. $class->reflFields[$field]->setValue($entity, $data[$field]);
  2550. $this->originalEntityData[$oid][$field] = $data[$field];
  2551. break;
  2552. }
  2553. $associatedId = [];
  2554. // TODO: Is this even computed right in all cases of composite keys?
  2555. foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
  2556. $joinColumnValue = $data[$srcColumn] ?? null;
  2557. if ($joinColumnValue !== null) {
  2558. if ($joinColumnValue instanceof BackedEnum) {
  2559. $joinColumnValue = $joinColumnValue->value;
  2560. }
  2561. if ($targetClass->containsForeignIdentifier) {
  2562. $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
  2563. } else {
  2564. $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
  2565. }
  2566. } elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) {
  2567. // the missing key is part of target's entity primary key
  2568. $associatedId = [];
  2569. break;
  2570. }
  2571. }
  2572. if (! $associatedId) {
  2573. // Foreign key is NULL
  2574. $class->reflFields[$field]->setValue($entity, null);
  2575. $this->originalEntityData[$oid][$field] = null;
  2576. break;
  2577. }
  2578. // Foreign key is set
  2579. // Check identity map first
  2580. // FIXME: Can break easily with composite keys if join column values are in
  2581. // wrong order. The correct order is the one in ClassMetadata#identifier.
  2582. $relatedIdHash = self::getIdHashByIdentifier($associatedId);
  2583. switch (true) {
  2584. case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
  2585. $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
  2586. // If this is an uninitialized proxy, we are deferring eager loads,
  2587. // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
  2588. // then we can append this entity for eager loading!
  2589. if (
  2590. $hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER &&
  2591. isset($hints[self::HINT_DEFEREAGERLOAD]) &&
  2592. ! $targetClass->isIdentifierComposite &&
  2593. $this->isUninitializedObject($newValue)
  2594. ) {
  2595. $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
  2596. }
  2597. break;
  2598. case $targetClass->subClasses:
  2599. // If it might be a subtype, it can not be lazy. There isn't even
  2600. // a way to solve this with deferred eager loading, which means putting
  2601. // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
  2602. $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
  2603. break;
  2604. default:
  2605. $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId);
  2606. switch (true) {
  2607. // We are negating the condition here. Other cases will assume it is valid!
  2608. case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER:
  2609. $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
  2610. $this->registerManaged($newValue, $associatedId, []);
  2611. break;
  2612. // Deferred eager load only works for single identifier classes
  2613. case isset($hints[self::HINT_DEFEREAGERLOAD]) &&
  2614. $hints[self::HINT_DEFEREAGERLOAD] &&
  2615. ! $targetClass->isIdentifierComposite:
  2616. // TODO: Is there a faster approach?
  2617. $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
  2618. $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
  2619. $this->registerManaged($newValue, $associatedId, []);
  2620. break;
  2621. default:
  2622. // TODO: This is very imperformant, ignore it?
  2623. $newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId);
  2624. break;
  2625. }
  2626. }
  2627. $this->originalEntityData[$oid][$field] = $newValue;
  2628. $class->reflFields[$field]->setValue($entity, $newValue);
  2629. if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE && $newValue !== null) {
  2630. $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
  2631. $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
  2632. }
  2633. break;
  2634. default:
  2635. // Ignore if its a cached collection
  2636. if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
  2637. break;
  2638. }
  2639. // use the given collection
  2640. if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
  2641. $data[$field]->setOwner($entity, $assoc);
  2642. $class->reflFields[$field]->setValue($entity, $data[$field]);
  2643. $this->originalEntityData[$oid][$field] = $data[$field];
  2644. break;
  2645. }
  2646. // Inject collection
  2647. $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
  2648. $pColl->setOwner($entity, $assoc);
  2649. $pColl->setInitialized(false);
  2650. $reflField = $class->reflFields[$field];
  2651. $reflField->setValue($entity, $pColl);
  2652. if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
  2653. if (
  2654. $assoc['type'] === ClassMetadata::ONE_TO_MANY
  2655. // is iteration
  2656. && ! (isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION])
  2657. // is foreign key composite
  2658. && ! ($targetClass->hasAssociation($assoc['mappedBy']) && count($targetClass->getAssociationMapping($assoc['mappedBy'])['joinColumns'] ?? []) > 1)
  2659. && ! isset($assoc['indexBy'])
  2660. ) {
  2661. $this->scheduleCollectionForBatchLoading($pColl, $class);
  2662. } else {
  2663. $this->loadCollection($pColl);
  2664. $pColl->takeSnapshot();
  2665. }
  2666. }
  2667. $this->originalEntityData[$oid][$field] = $pColl;
  2668. break;
  2669. }
  2670. }
  2671. // defer invoking of postLoad event to hydration complete step
  2672. $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
  2673. return $entity;
  2674. }
  2675. /** @return void */
  2676. public function triggerEagerLoads()
  2677. {
  2678. if (! $this->eagerLoadingEntities && ! $this->eagerLoadingCollections) {
  2679. return;
  2680. }
  2681. // avoid infinite recursion
  2682. $eagerLoadingEntities = $this->eagerLoadingEntities;
  2683. $this->eagerLoadingEntities = [];
  2684. foreach ($eagerLoadingEntities as $entityName => $ids) {
  2685. if (! $ids) {
  2686. continue;
  2687. }
  2688. $class = $this->em->getClassMetadata($entityName);
  2689. $batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize());
  2690. foreach ($batches as $batchedIds) {
  2691. $this->getEntityPersister($entityName)->loadAll(
  2692. array_combine($class->identifier, [$batchedIds])
  2693. );
  2694. }
  2695. }
  2696. $eagerLoadingCollections = $this->eagerLoadingCollections; // avoid recursion
  2697. $this->eagerLoadingCollections = [];
  2698. foreach ($eagerLoadingCollections as $group) {
  2699. $this->eagerLoadCollections($group['items'], $group['mapping']);
  2700. }
  2701. }
  2702. /**
  2703. * Load all data into the given collections, according to the specified mapping
  2704. *
  2705. * @param PersistentCollection[] $collections
  2706. * @param array<string, mixed> $mapping
  2707. * @phpstan-param array{
  2708. * targetEntity: class-string,
  2709. * sourceEntity: class-string,
  2710. * mappedBy: string,
  2711. * indexBy: string|null,
  2712. * orderBy: array<string, string>|null
  2713. * } $mapping
  2714. */
  2715. private function eagerLoadCollections(array $collections, array $mapping): void
  2716. {
  2717. $targetEntity = $mapping['targetEntity'];
  2718. $class = $this->em->getClassMetadata($mapping['sourceEntity']);
  2719. $mappedBy = $mapping['mappedBy'];
  2720. $batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), true);
  2721. foreach ($batches as $collectionBatch) {
  2722. $entities = [];
  2723. foreach ($collectionBatch as $collection) {
  2724. $entities[] = $collection->getOwner();
  2725. }
  2726. $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping['orderBy'] ?? null);
  2727. $targetClass = $this->em->getClassMetadata($targetEntity);
  2728. $targetProperty = $targetClass->getReflectionProperty($mappedBy);
  2729. foreach ($found as $targetValue) {
  2730. $sourceEntity = $targetProperty->getValue($targetValue);
  2731. if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]['joinColumns'])) {
  2732. // case where the hydration $targetValue itself has not yet fully completed, for example
  2733. // in case a bi-directional association is being hydrated and deferring eager loading is
  2734. // not possible due to subclassing.
  2735. $data = $this->getOriginalEntityData($targetValue);
  2736. $id = [];
  2737. foreach ($targetClass->associationMappings[$mappedBy]['joinColumns'] as $joinColumn) {
  2738. $id[] = $data[$joinColumn['name']];
  2739. }
  2740. } else {
  2741. $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
  2742. }
  2743. $idHash = implode(' ', $id);
  2744. if (isset($mapping['indexBy'])) {
  2745. $indexByProperty = $targetClass->getReflectionProperty($mapping['indexBy']);
  2746. $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
  2747. } else {
  2748. $collectionBatch[$idHash]->add($targetValue);
  2749. }
  2750. }
  2751. }
  2752. foreach ($collections as $association) {
  2753. $association->setInitialized(true);
  2754. $association->takeSnapshot();
  2755. }
  2756. }
  2757. /**
  2758. * Initializes (loads) an uninitialized persistent collection of an entity.
  2759. *
  2760. * @param PersistentCollection $collection The collection to initialize.
  2761. *
  2762. * @return void
  2763. *
  2764. * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
  2765. */
  2766. public function loadCollection(PersistentCollection $collection)
  2767. {
  2768. $assoc = $collection->getMapping();
  2769. $persister = $this->getEntityPersister($assoc['targetEntity']);
  2770. switch ($assoc['type']) {
  2771. case ClassMetadata::ONE_TO_MANY:
  2772. $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
  2773. break;
  2774. case ClassMetadata::MANY_TO_MANY:
  2775. $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
  2776. break;
  2777. }
  2778. $collection->setInitialized(true);
  2779. }
  2780. /**
  2781. * Schedule this collection for batch loading at the end of the UnitOfWork
  2782. */
  2783. private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass): void
  2784. {
  2785. $mapping = $collection->getMapping();
  2786. $name = $mapping['sourceEntity'] . '#' . $mapping['fieldName'];
  2787. if (! isset($this->eagerLoadingCollections[$name])) {
  2788. $this->eagerLoadingCollections[$name] = [
  2789. 'items' => [],
  2790. 'mapping' => $mapping,
  2791. ];
  2792. }
  2793. $owner = $collection->getOwner();
  2794. assert($owner !== null);
  2795. $id = $this->identifierFlattener->flattenIdentifier(
  2796. $sourceClass,
  2797. $sourceClass->getIdentifierValues($owner)
  2798. );
  2799. $idHash = implode(' ', $id);
  2800. $this->eagerLoadingCollections[$name]['items'][$idHash] = $collection;
  2801. }
  2802. /**
  2803. * Gets the identity map of the UnitOfWork.
  2804. *
  2805. * @return array<class-string, array<string, object>>
  2806. */
  2807. public function getIdentityMap()
  2808. {
  2809. return $this->identityMap;
  2810. }
  2811. /**
  2812. * Gets the original data of an entity. The original data is the data that was
  2813. * present at the time the entity was reconstituted from the database.
  2814. *
  2815. * @param object $entity
  2816. *
  2817. * @return mixed[]
  2818. * @phpstan-return array<string, mixed>
  2819. */
  2820. public function getOriginalEntityData($entity)
  2821. {
  2822. $oid = spl_object_id($entity);
  2823. return $this->originalEntityData[$oid] ?? [];
  2824. }
  2825. /**
  2826. * @param object $entity
  2827. * @param mixed[] $data
  2828. *
  2829. * @return void
  2830. *
  2831. * @ignore
  2832. */
  2833. public function setOriginalEntityData($entity, array $data)
  2834. {
  2835. $this->originalEntityData[spl_object_id($entity)] = $data;
  2836. }
  2837. /**
  2838. * INTERNAL:
  2839. * Sets a property value of the original data array of an entity.
  2840. *
  2841. * @param int $oid
  2842. * @param string $property
  2843. * @param mixed $value
  2844. *
  2845. * @return void
  2846. *
  2847. * @ignore
  2848. */
  2849. public function setOriginalEntityProperty($oid, $property, $value)
  2850. {
  2851. $this->originalEntityData[$oid][$property] = $value;
  2852. }
  2853. /**
  2854. * Gets the identifier of an entity.
  2855. * The returned value is always an array of identifier values. If the entity
  2856. * has a composite identifier then the identifier values are in the same
  2857. * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
  2858. *
  2859. * @param object $entity
  2860. *
  2861. * @return mixed[] The identifier values.
  2862. */
  2863. public function getEntityIdentifier($entity)
  2864. {
  2865. if (! isset($this->entityIdentifiers[spl_object_id($entity)])) {
  2866. throw EntityNotFoundException::noIdentifierFound(get_debug_type($entity));
  2867. }
  2868. return $this->entityIdentifiers[spl_object_id($entity)];
  2869. }
  2870. /**
  2871. * Processes an entity instance to extract their identifier values.
  2872. *
  2873. * @param object $entity The entity instance.
  2874. *
  2875. * @return mixed A scalar value.
  2876. *
  2877. * @throws ORMInvalidArgumentException
  2878. */
  2879. public function getSingleIdentifierValue($entity)
  2880. {
  2881. $class = $this->em->getClassMetadata(get_class($entity));
  2882. if ($class->isIdentifierComposite) {
  2883. throw ORMInvalidArgumentException::invalidCompositeIdentifier();
  2884. }
  2885. $values = $this->isInIdentityMap($entity)
  2886. ? $this->getEntityIdentifier($entity)
  2887. : $class->getIdentifierValues($entity);
  2888. return $values[$class->identifier[0]] ?? null;
  2889. }
  2890. /**
  2891. * Tries to find an entity with the given identifier in the identity map of
  2892. * this UnitOfWork.
  2893. *
  2894. * @param mixed $id The entity identifier to look for.
  2895. * @param class-string $rootClassName The name of the root class of the mapped entity hierarchy.
  2896. *
  2897. * @return object|false Returns the entity with the specified identifier if it exists in
  2898. * this UnitOfWork, FALSE otherwise.
  2899. */
  2900. public function tryGetById($id, $rootClassName)
  2901. {
  2902. $idHash = self::getIdHashByIdentifier((array) $id);
  2903. return $this->identityMap[$rootClassName][$idHash] ?? false;
  2904. }
  2905. /**
  2906. * Schedules an entity for dirty-checking at commit-time.
  2907. *
  2908. * @param object $entity The entity to schedule for dirty-checking.
  2909. *
  2910. * @return void
  2911. *
  2912. * @todo Rename: scheduleForSynchronization
  2913. */
  2914. public function scheduleForDirtyCheck($entity)
  2915. {
  2916. $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
  2917. $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
  2918. }
  2919. /**
  2920. * Checks whether the UnitOfWork has any pending insertions.
  2921. *
  2922. * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
  2923. */
  2924. public function hasPendingInsertions()
  2925. {
  2926. return ! empty($this->entityInsertions);
  2927. }
  2928. /**
  2929. * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
  2930. * number of entities in the identity map.
  2931. *
  2932. * @return int
  2933. */
  2934. public function size()
  2935. {
  2936. return array_sum(array_map('count', $this->identityMap));
  2937. }
  2938. /**
  2939. * Gets the EntityPersister for an Entity.
  2940. *
  2941. * @param class-string $entityName The name of the Entity.
  2942. *
  2943. * @return EntityPersister
  2944. */
  2945. public function getEntityPersister($entityName)
  2946. {
  2947. if (isset($this->persisters[$entityName])) {
  2948. return $this->persisters[$entityName];
  2949. }
  2950. $class = $this->em->getClassMetadata($entityName);
  2951. switch (true) {
  2952. case $class->isInheritanceTypeNone():
  2953. $persister = new BasicEntityPersister($this->em, $class);
  2954. break;
  2955. case $class->isInheritanceTypeSingleTable():
  2956. $persister = new SingleTablePersister($this->em, $class);
  2957. break;
  2958. case $class->isInheritanceTypeJoined():
  2959. $persister = new JoinedSubclassPersister($this->em, $class);
  2960. break;
  2961. default:
  2962. throw new RuntimeException('No persister found for entity.');
  2963. }
  2964. if ($this->hasCache && $class->cache !== null) {
  2965. $persister = $this->em->getConfiguration()
  2966. ->getSecondLevelCacheConfiguration()
  2967. ->getCacheFactory()
  2968. ->buildCachedEntityPersister($this->em, $persister, $class);
  2969. }
  2970. $this->persisters[$entityName] = $persister;
  2971. return $this->persisters[$entityName];
  2972. }
  2973. /**
  2974. * Gets a collection persister for a collection-valued association.
  2975. *
  2976. * @phpstan-param AssociationMapping $association
  2977. *
  2978. * @return CollectionPersister
  2979. */
  2980. public function getCollectionPersister(array $association)
  2981. {
  2982. $role = isset($association['cache'])
  2983. ? $association['sourceEntity'] . '::' . $association['fieldName']
  2984. : $association['type'];
  2985. if (isset($this->collectionPersisters[$role])) {
  2986. return $this->collectionPersisters[$role];
  2987. }
  2988. $persister = $association['type'] === ClassMetadata::ONE_TO_MANY
  2989. ? new OneToManyPersister($this->em)
  2990. : new ManyToManyPersister($this->em);
  2991. if ($this->hasCache && isset($association['cache'])) {
  2992. $persister = $this->em->getConfiguration()
  2993. ->getSecondLevelCacheConfiguration()
  2994. ->getCacheFactory()
  2995. ->buildCachedCollectionPersister($this->em, $persister, $association);
  2996. }
  2997. $this->collectionPersisters[$role] = $persister;
  2998. return $this->collectionPersisters[$role];
  2999. }
  3000. /**
  3001. * INTERNAL:
  3002. * Registers an entity as managed.
  3003. *
  3004. * @param object $entity The entity.
  3005. * @param mixed[] $id The identifier values.
  3006. * @param mixed[] $data The original entity data.
  3007. *
  3008. * @return void
  3009. */
  3010. public function registerManaged($entity, array $id, array $data)
  3011. {
  3012. $oid = spl_object_id($entity);
  3013. $this->entityIdentifiers[$oid] = $id;
  3014. $this->entityStates[$oid] = self::STATE_MANAGED;
  3015. $this->originalEntityData[$oid] = $data;
  3016. $this->addToIdentityMap($entity);
  3017. if ($entity instanceof NotifyPropertyChanged && ! $this->isUninitializedObject($entity)) {
  3018. $entity->addPropertyChangedListener($this);
  3019. }
  3020. }
  3021. /**
  3022. * INTERNAL:
  3023. * Clears the property changeset of the entity with the given OID.
  3024. *
  3025. * @param int $oid The entity's OID.
  3026. *
  3027. * @return void
  3028. */
  3029. public function clearEntityChangeSet($oid)
  3030. {
  3031. unset($this->entityChangeSets[$oid]);
  3032. }
  3033. /* PropertyChangedListener implementation */
  3034. /**
  3035. * Notifies this UnitOfWork of a property change in an entity.
  3036. *
  3037. * @param object $sender The entity that owns the property.
  3038. * @param string $propertyName The name of the property that changed.
  3039. * @param mixed $oldValue The old value of the property.
  3040. * @param mixed $newValue The new value of the property.
  3041. *
  3042. * @return void
  3043. */
  3044. public function propertyChanged($sender, $propertyName, $oldValue, $newValue)
  3045. {
  3046. $oid = spl_object_id($sender);
  3047. $class = $this->em->getClassMetadata(get_class($sender));
  3048. $isAssocField = isset($class->associationMappings[$propertyName]);
  3049. if (! $isAssocField && ! isset($class->fieldMappings[$propertyName])) {
  3050. return; // ignore non-persistent fields
  3051. }
  3052. // Update changeset and mark entity for synchronization
  3053. $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
  3054. if (! isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
  3055. $this->scheduleForDirtyCheck($sender);
  3056. }
  3057. }
  3058. /**
  3059. * Gets the currently scheduled entity insertions in this UnitOfWork.
  3060. *
  3061. * @phpstan-return array<int, object>
  3062. */
  3063. public function getScheduledEntityInsertions()
  3064. {
  3065. return $this->entityInsertions;
  3066. }
  3067. /**
  3068. * Gets the currently scheduled entity updates in this UnitOfWork.
  3069. *
  3070. * @phpstan-return array<int, object>
  3071. */
  3072. public function getScheduledEntityUpdates()
  3073. {
  3074. return $this->entityUpdates;
  3075. }
  3076. /**
  3077. * Gets the currently scheduled entity deletions in this UnitOfWork.
  3078. *
  3079. * @phpstan-return array<int, object>
  3080. */
  3081. public function getScheduledEntityDeletions()
  3082. {
  3083. return $this->entityDeletions;
  3084. }
  3085. /**
  3086. * Gets the currently scheduled complete collection deletions
  3087. *
  3088. * @phpstan-return array<int, PersistentCollection<array-key, object>>
  3089. */
  3090. public function getScheduledCollectionDeletions()
  3091. {
  3092. return $this->collectionDeletions;
  3093. }
  3094. /**
  3095. * Gets the currently scheduled collection inserts, updates and deletes.
  3096. *
  3097. * @phpstan-return array<int, PersistentCollection<array-key, object>>
  3098. */
  3099. public function getScheduledCollectionUpdates()
  3100. {
  3101. return $this->collectionUpdates;
  3102. }
  3103. /**
  3104. * Helper method to initialize a lazy loading proxy or persistent collection.
  3105. *
  3106. * @param object $obj
  3107. *
  3108. * @return void
  3109. */
  3110. public function initializeObject($obj)
  3111. {
  3112. if ($obj instanceof InternalProxy) {
  3113. $obj->__load();
  3114. return;
  3115. }
  3116. if ($obj instanceof PersistentCollection) {
  3117. $obj->initialize();
  3118. }
  3119. }
  3120. /**
  3121. * Tests if a value is an uninitialized entity.
  3122. *
  3123. * @param mixed $obj
  3124. *
  3125. * @phpstan-assert-if-true InternalProxy $obj
  3126. */
  3127. public function isUninitializedObject($obj): bool
  3128. {
  3129. return $obj instanceof InternalProxy && ! $obj->__isInitialized();
  3130. }
  3131. /**
  3132. * Helper method to show an object as string.
  3133. *
  3134. * @param object $obj
  3135. */
  3136. private static function objToStr($obj): string
  3137. {
  3138. return method_exists($obj, '__toString') ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj);
  3139. }
  3140. /**
  3141. * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
  3142. *
  3143. * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
  3144. * on this object that might be necessary to perform a correct update.
  3145. *
  3146. * @param object $object
  3147. *
  3148. * @return void
  3149. *
  3150. * @throws ORMInvalidArgumentException
  3151. */
  3152. public function markReadOnly($object)
  3153. {
  3154. if (! is_object($object) || ! $this->isInIdentityMap($object)) {
  3155. throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
  3156. }
  3157. $this->readOnlyObjects[spl_object_id($object)] = true;
  3158. }
  3159. /**
  3160. * Is this entity read only?
  3161. *
  3162. * @param object $object
  3163. *
  3164. * @return bool
  3165. *
  3166. * @throws ORMInvalidArgumentException
  3167. */
  3168. public function isReadOnly($object)
  3169. {
  3170. if (! is_object($object)) {
  3171. throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
  3172. }
  3173. return isset($this->readOnlyObjects[spl_object_id($object)]);
  3174. }
  3175. /**
  3176. * Perform whatever processing is encapsulated here after completion of the transaction.
  3177. */
  3178. private function afterTransactionComplete(): void
  3179. {
  3180. $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
  3181. $persister->afterTransactionComplete();
  3182. });
  3183. }
  3184. /**
  3185. * Perform whatever processing is encapsulated here after completion of the rolled-back.
  3186. */
  3187. private function afterTransactionRolledBack(): void
  3188. {
  3189. $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
  3190. $persister->afterTransactionRolledBack();
  3191. });
  3192. }
  3193. /**
  3194. * Performs an action after the transaction.
  3195. */
  3196. private function performCallbackOnCachedPersister(callable $callback): void
  3197. {
  3198. if (! $this->hasCache) {
  3199. return;
  3200. }
  3201. foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
  3202. if ($persister instanceof CachedPersister) {
  3203. $callback($persister);
  3204. }
  3205. }
  3206. }
  3207. private function dispatchOnFlushEvent(): void
  3208. {
  3209. if ($this->evm->hasListeners(Events::onFlush)) {
  3210. $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
  3211. }
  3212. }
  3213. private function dispatchPostFlushEvent(): void
  3214. {
  3215. if ($this->evm->hasListeners(Events::postFlush)) {
  3216. $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
  3217. }
  3218. }
  3219. /**
  3220. * Verifies if two given entities actually are the same based on identifier comparison
  3221. *
  3222. * @param object $entity1
  3223. * @param object $entity2
  3224. */
  3225. private function isIdentifierEquals($entity1, $entity2): bool
  3226. {
  3227. if ($entity1 === $entity2) {
  3228. return true;
  3229. }
  3230. $class = $this->em->getClassMetadata(get_class($entity1));
  3231. if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
  3232. return false;
  3233. }
  3234. $oid1 = spl_object_id($entity1);
  3235. $oid2 = spl_object_id($entity2);
  3236. $id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
  3237. $id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
  3238. return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2);
  3239. }
  3240. /** @throws ORMInvalidArgumentException */
  3241. private function assertThatThereAreNoUnintentionallyNonPersistedAssociations(): void
  3242. {
  3243. $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
  3244. $this->nonCascadedNewDetectedEntities = [];
  3245. if ($entitiesNeedingCascadePersist) {
  3246. throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
  3247. array_values($entitiesNeedingCascadePersist)
  3248. );
  3249. }
  3250. }
  3251. /**
  3252. * @param object $entity
  3253. * @param object $managedCopy
  3254. *
  3255. * @throws ORMException
  3256. * @throws OptimisticLockException
  3257. * @throws TransactionRequiredException
  3258. */
  3259. private function mergeEntityStateIntoManagedCopy($entity, $managedCopy): void
  3260. {
  3261. if ($this->isUninitializedObject($entity)) {
  3262. return;
  3263. }
  3264. $this->initializeObject($managedCopy);
  3265. $class = $this->em->getClassMetadata(get_class($entity));
  3266. foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
  3267. $name = $prop->name;
  3268. if (PHP_VERSION_ID < 80100) {
  3269. $prop->setAccessible(true);
  3270. }
  3271. if (! isset($class->associationMappings[$name])) {
  3272. if (! $class->isIdentifier($name)) {
  3273. $prop->setValue($managedCopy, $prop->getValue($entity));
  3274. }
  3275. } else {
  3276. $assoc2 = $class->associationMappings[$name];
  3277. if ($assoc2['type'] & ClassMetadata::TO_ONE) {
  3278. $other = $prop->getValue($entity);
  3279. if ($other === null) {
  3280. $prop->setValue($managedCopy, null);
  3281. } else {
  3282. if ($this->isUninitializedObject($other)) {
  3283. // do not merge fields marked lazy that have not been fetched.
  3284. continue;
  3285. }
  3286. if (! $assoc2['isCascadeMerge']) {
  3287. if ($this->getEntityState($other) === self::STATE_DETACHED) {
  3288. $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
  3289. $relatedId = $targetClass->getIdentifierValues($other);
  3290. $other = $this->tryGetById($relatedId, $targetClass->name);
  3291. if (! $other) {
  3292. if ($targetClass->subClasses) {
  3293. $other = $this->em->find($targetClass->name, $relatedId);
  3294. } else {
  3295. $other = $this->em->getProxyFactory()->getProxy(
  3296. $assoc2['targetEntity'],
  3297. $relatedId
  3298. );
  3299. $this->registerManaged($other, $relatedId, []);
  3300. }
  3301. }
  3302. }
  3303. $prop->setValue($managedCopy, $other);
  3304. }
  3305. }
  3306. } else {
  3307. $mergeCol = $prop->getValue($entity);
  3308. if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
  3309. // do not merge fields marked lazy that have not been fetched.
  3310. // keep the lazy persistent collection of the managed copy.
  3311. continue;
  3312. }
  3313. $managedCol = $prop->getValue($managedCopy);
  3314. if (! $managedCol) {
  3315. $managedCol = new PersistentCollection(
  3316. $this->em,
  3317. $this->em->getClassMetadata($assoc2['targetEntity']),
  3318. new ArrayCollection()
  3319. );
  3320. $managedCol->setOwner($managedCopy, $assoc2);
  3321. $prop->setValue($managedCopy, $managedCol);
  3322. }
  3323. if ($assoc2['isCascadeMerge']) {
  3324. $managedCol->initialize();
  3325. // clear and set dirty a managed collection if its not also the same collection to merge from.
  3326. if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
  3327. $managedCol->unwrap()->clear();
  3328. $managedCol->setDirty(true);
  3329. if (
  3330. $assoc2['isOwningSide']
  3331. && $assoc2['type'] === ClassMetadata::MANY_TO_MANY
  3332. && $class->isChangeTrackingNotify()
  3333. ) {
  3334. $this->scheduleForDirtyCheck($managedCopy);
  3335. }
  3336. }
  3337. }
  3338. }
  3339. }
  3340. if ($class->isChangeTrackingNotify()) {
  3341. // Just treat all properties as changed, there is no other choice.
  3342. $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
  3343. }
  3344. }
  3345. }
  3346. /**
  3347. * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
  3348. * Unit of work able to fire deferred events, related to loading events here.
  3349. *
  3350. * @internal should be called internally from object hydrators
  3351. *
  3352. * @return void
  3353. */
  3354. public function hydrationComplete()
  3355. {
  3356. $this->hydrationCompleteHandler->hydrationComplete();
  3357. }
  3358. private function clearIdentityMapForEntityName(string $entityName): void
  3359. {
  3360. if (! isset($this->identityMap[$entityName])) {
  3361. return;
  3362. }
  3363. $visited = [];
  3364. foreach ($this->identityMap[$entityName] as $entity) {
  3365. $this->doDetach($entity, $visited, false);
  3366. }
  3367. }
  3368. private function clearEntityInsertionsForEntityName(string $entityName): void
  3369. {
  3370. foreach ($this->entityInsertions as $hash => $entity) {
  3371. // note: performance optimization - `instanceof` is much faster than a function call
  3372. if ($entity instanceof $entityName && get_class($entity) === $entityName) {
  3373. unset($this->entityInsertions[$hash]);
  3374. }
  3375. }
  3376. }
  3377. /**
  3378. * @param mixed $identifierValue
  3379. *
  3380. * @return mixed the identifier after type conversion
  3381. *
  3382. * @throws MappingException if the entity has more than a single identifier.
  3383. */
  3384. private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
  3385. {
  3386. return $this->em->getConnection()->convertToPHPValue(
  3387. $identifierValue,
  3388. $class->getTypeOfField($class->getSingleIdentifierFieldName())
  3389. );
  3390. }
  3391. /**
  3392. * Given a flat identifier, this method will produce another flat identifier, but with all
  3393. * association fields that are mapped as identifiers replaced by entity references, recursively.
  3394. *
  3395. * @param mixed[] $flatIdentifier
  3396. *
  3397. * @return array<string, mixed>
  3398. */
  3399. private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier): array
  3400. {
  3401. $normalizedAssociatedId = [];
  3402. foreach ($targetClass->getIdentifierFieldNames() as $name) {
  3403. if (! array_key_exists($name, $flatIdentifier)) {
  3404. continue;
  3405. }
  3406. if (! $targetClass->isSingleValuedAssociation($name)) {
  3407. $normalizedAssociatedId[$name] = $flatIdentifier[$name];
  3408. continue;
  3409. }
  3410. $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name));
  3411. // Note: the ORM prevents using an entity with a composite identifier as an identifier association
  3412. // therefore, reset($targetIdMetadata->identifier) is always correct
  3413. $normalizedAssociatedId[$name] = $this->em->getReference(
  3414. $targetIdMetadata->getName(),
  3415. $this->normalizeIdentifier(
  3416. $targetIdMetadata,
  3417. [(string) reset($targetIdMetadata->identifier) => $flatIdentifier[$name]]
  3418. )
  3419. );
  3420. }
  3421. return $normalizedAssociatedId;
  3422. }
  3423. /**
  3424. * Assign a post-insert generated ID to an entity
  3425. *
  3426. * This is used by EntityPersisters after they inserted entities into the database.
  3427. * It will place the assigned ID values in the entity's fields and start tracking
  3428. * the entity in the identity map.
  3429. *
  3430. * @param object $entity
  3431. * @param mixed $generatedId
  3432. */
  3433. final public function assignPostInsertId($entity, $generatedId): void
  3434. {
  3435. $class = $this->em->getClassMetadata(get_class($entity));
  3436. $idField = $class->getSingleIdentifierFieldName();
  3437. $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
  3438. $oid = spl_object_id($entity);
  3439. $class->reflFields[$idField]->setValue($entity, $idValue);
  3440. $this->entityIdentifiers[$oid] = [$idField => $idValue];
  3441. $this->entityStates[$oid] = self::STATE_MANAGED;
  3442. $this->originalEntityData[$oid][$idField] = $idValue;
  3443. $this->addToIdentityMap($entity);
  3444. }
  3445. }