vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php line 3643

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