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

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