PageRenderTime 38ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/Doctrine/ORM/UnitOfWork.php

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