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

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