PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.36.0
MailPoet – Newsletters, Email Marketing, and Automation v5.36.0
5.36.0 5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / vendor-prefixed / doctrine / orm / src / UnitOfWork.php
mailpoet / vendor-prefixed / doctrine / orm / src Last commit date
Cache 9 months ago Decorator 9 months ago Event 9 months ago Exception 9 months ago Id 9 months ago Internal 9 months ago Mapping 9 months ago Persisters 8 months ago Proxy 9 months ago Query 9 months ago Repository 9 months ago Tools 9 months ago Utility 9 months ago AbstractQuery.php 9 months ago Cache.php 9 months ago Configuration.php 9 months ago EntityManager.php 9 months ago EntityManagerInterface.php 9 months ago EntityNotFoundException.php 9 months ago EntityRepository.php 9 months ago Events.php 9 months ago LazyCriteriaCollection.php 9 months ago NativeQuery.php 9 months ago NoResultException.php 9 months ago NonUniqueResultException.php 9 months ago ORMException.php 9 months ago ORMInvalidArgumentException.php 9 months ago ORMSetup.php 9 months ago OptimisticLockException.php 9 months ago PersistentCollection.php 9 months ago PessimisticLockException.php 9 months ago Query.php 9 months ago QueryBuilder.php 9 months ago TransactionRequiredException.php 9 months ago UnexpectedResultException.php 9 months ago UnitOfWork.php 9 months ago Version.php 9 months ago index.php 9 months ago
UnitOfWork.php
2214 lines
1 <?php
2 declare (strict_types=1);
3 namespace MailPoetVendor\Doctrine\ORM;
4 if (!defined('ABSPATH')) exit;
5 use BackedEnum;
6 use DateTimeInterface;
7 use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection;
8 use MailPoetVendor\Doctrine\Common\Collections\Collection;
9 use MailPoetVendor\Doctrine\Common\EventManager;
10 use MailPoetVendor\Doctrine\DBAL\Connections\PrimaryReadReplicaConnection;
11 use MailPoetVendor\Doctrine\DBAL\LockMode;
12 use MailPoetVendor\Doctrine\Deprecations\Deprecation;
13 use MailPoetVendor\Doctrine\ORM\Cache\Persister\CachedPersister;
14 use MailPoetVendor\Doctrine\ORM\Event\ListenersInvoker;
15 use MailPoetVendor\Doctrine\ORM\Event\OnFlushEventArgs;
16 use MailPoetVendor\Doctrine\ORM\Event\PostFlushEventArgs;
17 use MailPoetVendor\Doctrine\ORM\Event\PostPersistEventArgs;
18 use MailPoetVendor\Doctrine\ORM\Event\PostRemoveEventArgs;
19 use MailPoetVendor\Doctrine\ORM\Event\PostUpdateEventArgs;
20 use MailPoetVendor\Doctrine\ORM\Event\PreFlushEventArgs;
21 use MailPoetVendor\Doctrine\ORM\Event\PrePersistEventArgs;
22 use MailPoetVendor\Doctrine\ORM\Event\PreRemoveEventArgs;
23 use MailPoetVendor\Doctrine\ORM\Event\PreUpdateEventArgs;
24 use MailPoetVendor\Doctrine\ORM\Exception\EntityIdentityCollisionException;
25 use MailPoetVendor\Doctrine\ORM\Exception\ORMException;
26 use MailPoetVendor\Doctrine\ORM\Exception\UnexpectedAssociationValue;
27 use MailPoetVendor\Doctrine\ORM\Id\AssignedGenerator;
28 use MailPoetVendor\Doctrine\ORM\Internal\HydrationCompleteHandler;
29 use MailPoetVendor\Doctrine\ORM\Internal\StronglyConnectedComponents;
30 use MailPoetVendor\Doctrine\ORM\Internal\TopologicalSort;
31 use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata;
32 use MailPoetVendor\Doctrine\ORM\Mapping\MappingException;
33 use MailPoetVendor\Doctrine\ORM\Mapping\Reflection\ReflectionPropertiesGetter;
34 use MailPoetVendor\Doctrine\ORM\Persisters\Collection\CollectionPersister;
35 use MailPoetVendor\Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
36 use MailPoetVendor\Doctrine\ORM\Persisters\Collection\OneToManyPersister;
37 use MailPoetVendor\Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
38 use MailPoetVendor\Doctrine\ORM\Persisters\Entity\EntityPersister;
39 use MailPoetVendor\Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
40 use MailPoetVendor\Doctrine\ORM\Persisters\Entity\SingleTablePersister;
41 use MailPoetVendor\Doctrine\ORM\Proxy\InternalProxy;
42 use MailPoetVendor\Doctrine\ORM\Utility\IdentifierFlattener;
43 use MailPoetVendor\Doctrine\Persistence\Mapping\RuntimeReflectionService;
44 use MailPoetVendor\Doctrine\Persistence\NotifyPropertyChanged;
45 use MailPoetVendor\Doctrine\Persistence\ObjectManagerAware;
46 use MailPoetVendor\Doctrine\Persistence\PropertyChangedListener;
47 use Exception;
48 use InvalidArgumentException;
49 use RuntimeException;
50 use MailPoetVendor\Symfony\Component\VarExporter\Hydrator;
51 use UnexpectedValueException;
52 use function array_chunk;
53 use function array_combine;
54 use function array_diff_key;
55 use function array_filter;
56 use function array_key_exists;
57 use function array_map;
58 use function array_merge;
59 use function array_sum;
60 use function array_values;
61 use function assert;
62 use function current;
63 use function func_get_arg;
64 use function func_num_args;
65 use function get_class;
66 use function get_debug_type;
67 use function implode;
68 use function in_array;
69 use function is_array;
70 use function is_object;
71 use function method_exists;
72 use function reset;
73 use function spl_object_id;
74 use function sprintf;
75 use function strtolower;
76 use const PHP_VERSION_ID;
77 class UnitOfWork implements PropertyChangedListener
78 {
79 public const STATE_MANAGED = 1;
80 public const STATE_NEW = 2;
81 public const STATE_DETACHED = 3;
82 public const STATE_REMOVED = 4;
83 public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
84 private $identityMap = [];
85 private $entityIdentifiers = [];
86 private $originalEntityData = [];
87 private $entityChangeSets = [];
88 private $entityStates = [];
89 private $scheduledForSynchronization = [];
90 private $entityInsertions = [];
91 private $entityUpdates = [];
92 private $extraUpdates = [];
93 private $entityDeletions = [];
94 private $nonCascadedNewDetectedEntities = [];
95 private $collectionDeletions = [];
96 private $collectionUpdates = [];
97 private $visitedCollections = [];
98 private $pendingCollectionElementRemovals = [];
99 private $em;
100 private $persisters = [];
101 private $collectionPersisters = [];
102 private $evm;
103 private $listenersInvoker;
104 private $identifierFlattener;
105 private $orphanRemovals = [];
106 private $readOnlyObjects = [];
107 private $eagerLoadingEntities = [];
108 private $eagerLoadingCollections = [];
109 protected $hasCache = \false;
110 private $hydrationCompleteHandler;
111 private $reflectionPropertiesGetter;
112 public function __construct(EntityManagerInterface $em)
113 {
114 $this->em = $em;
115 $this->evm = $em->getEventManager();
116 $this->listenersInvoker = new ListenersInvoker($em);
117 $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled();
118 $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory());
119 $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
120 $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService());
121 }
122 public function commit($entity = null)
123 {
124 if ($entity !== null) {
125 Deprecation::triggerIfCalledFromOutside('doctrine/orm', 'https://github.com/doctrine/orm/issues/8459', 'Calling %s() with any arguments to commit specific entities is deprecated and will not be supported in Doctrine ORM 3.0.', __METHOD__);
126 }
127 $connection = $this->em->getConnection();
128 if ($connection instanceof PrimaryReadReplicaConnection) {
129 $connection->ensureConnectedToPrimary();
130 }
131 // Raise preFlush
132 if ($this->evm->hasListeners(Events::preFlush)) {
133 $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
134 }
135 // Compute changes done since last commit.
136 if ($entity === null) {
137 $this->computeChangeSets();
138 } elseif (is_object($entity)) {
139 $this->computeSingleEntityChangeSet($entity);
140 } elseif (is_array($entity)) {
141 foreach ($entity as $object) {
142 $this->computeSingleEntityChangeSet($object);
143 }
144 }
145 if (!($this->entityInsertions || $this->entityDeletions || $this->entityUpdates || $this->collectionUpdates || $this->collectionDeletions || $this->orphanRemovals)) {
146 $this->dispatchOnFlushEvent();
147 $this->dispatchPostFlushEvent();
148 $this->postCommitCleanup($entity);
149 return;
150 // Nothing to do.
151 }
152 $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
153 if ($this->orphanRemovals) {
154 foreach ($this->orphanRemovals as $orphan) {
155 $this->remove($orphan);
156 }
157 }
158 $this->dispatchOnFlushEvent();
159 $conn = $this->em->getConnection();
160 $conn->beginTransaction();
161 $successful = \false;
162 try {
163 // Collection deletions (deletions of complete collections)
164 foreach ($this->collectionDeletions as $collectionToDelete) {
165 // Deferred explicit tracked collections can be removed only when owning relation was persisted
166 $owner = $collectionToDelete->getOwner();
167 if ($this->em->getClassMetadata(get_class($owner))->isChangeTrackingDeferredImplicit() || $this->isScheduledForDirtyCheck($owner)) {
168 $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
169 }
170 }
171 if ($this->entityInsertions) {
172 // Perform entity insertions first, so that all new entities have their rows in the database
173 // and can be referred to by foreign keys. The commit order only needs to take new entities
174 // into account (new entities referring to other new entities), since all other types (entities
175 // with updates or scheduled deletions) are currently not a problem, since they are already
176 // in the database.
177 $this->executeInserts();
178 }
179 if ($this->entityUpdates) {
180 // Updates do not need to follow a particular order
181 $this->executeUpdates();
182 }
183 // Extra updates that were requested by persisters.
184 // This may include foreign keys that could not be set when an entity was inserted,
185 // which may happen in the case of circular foreign key relationships.
186 if ($this->extraUpdates) {
187 $this->executeExtraUpdates();
188 }
189 // Collection updates (deleteRows, updateRows, insertRows)
190 // No particular order is necessary, since all entities themselves are already
191 // in the database
192 foreach ($this->collectionUpdates as $collectionToUpdate) {
193 $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
194 }
195 // Entity deletions come last. Their order only needs to take care of other deletions
196 // (first delete entities depending upon others, before deleting depended-upon entities).
197 if ($this->entityDeletions) {
198 $this->executeDeletions();
199 }
200 // Commit failed silently
201 if ($conn->commit() === \false) {
202 $object = is_object($entity) ? $entity : null;
203 throw new OptimisticLockException('Commit failed', $object);
204 }
205 $successful = \true;
206 } finally {
207 if (!$successful) {
208 $this->em->close();
209 if ($conn->isTransactionActive()) {
210 $conn->rollBack();
211 }
212 $this->afterTransactionRolledBack();
213 }
214 }
215 $this->afterTransactionComplete();
216 // Unset removed entities from collections, and take new snapshots from
217 // all visited collections.
218 foreach ($this->visitedCollections as $coid => $coll) {
219 if (isset($this->pendingCollectionElementRemovals[$coid])) {
220 foreach ($this->pendingCollectionElementRemovals[$coid] as $key => $valueIgnored) {
221 unset($coll[$key]);
222 }
223 }
224 $coll->takeSnapshot();
225 }
226 $this->dispatchPostFlushEvent();
227 $this->postCommitCleanup($entity);
228 }
229 private function postCommitCleanup($entity) : void
230 {
231 $this->entityInsertions = $this->entityUpdates = $this->entityDeletions = $this->extraUpdates = $this->collectionUpdates = $this->nonCascadedNewDetectedEntities = $this->collectionDeletions = $this->pendingCollectionElementRemovals = $this->visitedCollections = $this->orphanRemovals = [];
232 if ($entity === null) {
233 $this->entityChangeSets = $this->scheduledForSynchronization = [];
234 return;
235 }
236 $entities = is_object($entity) ? [$entity] : $entity;
237 foreach ($entities as $object) {
238 $oid = spl_object_id($object);
239 $this->clearEntityChangeSet($oid);
240 unset($this->scheduledForSynchronization[$this->em->getClassMetadata(get_class($object))->rootEntityName][$oid]);
241 }
242 }
243 private function computeScheduleInsertsChangeSets() : void
244 {
245 foreach ($this->entityInsertions as $entity) {
246 $class = $this->em->getClassMetadata(get_class($entity));
247 $this->computeChangeSet($class, $entity);
248 }
249 }
250 private function computeSingleEntityChangeSet($entity) : void
251 {
252 $state = $this->getEntityState($entity);
253 if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
254 throw new InvalidArgumentException('Entity has to be managed or scheduled for removal for single computation ' . self::objToStr($entity));
255 }
256 $class = $this->em->getClassMetadata(get_class($entity));
257 if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
258 $this->persist($entity);
259 }
260 // Compute changes for INSERTed entities first. This must always happen even in this case.
261 $this->computeScheduleInsertsChangeSets();
262 if ($class->isReadOnly) {
263 return;
264 }
265 // Ignore uninitialized proxy objects
266 if ($this->isUninitializedObject($entity)) {
267 return;
268 }
269 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
270 $oid = spl_object_id($entity);
271 if (!isset($this->entityInsertions[$oid]) && !isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
272 $this->computeChangeSet($class, $entity);
273 }
274 }
275 private function executeExtraUpdates() : void
276 {
277 foreach ($this->extraUpdates as $oid => $update) {
278 [$entity, $changeset] = $update;
279 $this->entityChangeSets[$oid] = $changeset;
280 $this->getEntityPersister(get_class($entity))->update($entity);
281 }
282 $this->extraUpdates = [];
283 }
284 public function &getEntityChangeSet($entity)
285 {
286 $oid = spl_object_id($entity);
287 $data = [];
288 if (!isset($this->entityChangeSets[$oid])) {
289 return $data;
290 }
291 return $this->entityChangeSets[$oid];
292 }
293 public function computeChangeSet(ClassMetadata $class, $entity)
294 {
295 $oid = spl_object_id($entity);
296 if (isset($this->readOnlyObjects[$oid])) {
297 return;
298 }
299 if (!$class->isInheritanceTypeNone()) {
300 $class = $this->em->getClassMetadata(get_class($entity));
301 }
302 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
303 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
304 $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
305 }
306 $actualData = [];
307 foreach ($class->reflFields as $name => $refProp) {
308 $value = $refProp->getValue($entity);
309 if ($class->isCollectionValuedAssociation($name) && $value !== null) {
310 if ($value instanceof PersistentCollection) {
311 if ($value->getOwner() === $entity) {
312 $actualData[$name] = $value;
313 continue;
314 }
315 $value = new ArrayCollection($value->getValues());
316 }
317 // If $value is not a Collection then use an ArrayCollection.
318 if (!$value instanceof Collection) {
319 $value = new ArrayCollection($value);
320 }
321 $assoc = $class->associationMappings[$name];
322 // Inject PersistentCollection
323 $value = new PersistentCollection($this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value);
324 $value->setOwner($entity, $assoc);
325 $value->setDirty(!$value->isEmpty());
326 $refProp->setValue($entity, $value);
327 $actualData[$name] = $value;
328 continue;
329 }
330 if ((!$class->isIdentifier($name) || !$class->isIdGeneratorIdentity()) && $name !== $class->versionField) {
331 $actualData[$name] = $value;
332 }
333 }
334 if (!isset($this->originalEntityData[$oid])) {
335 // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
336 // These result in an INSERT.
337 $this->originalEntityData[$oid] = $actualData;
338 $changeSet = [];
339 foreach ($actualData as $propName => $actualValue) {
340 if (!isset($class->associationMappings[$propName])) {
341 $changeSet[$propName] = [null, $actualValue];
342 continue;
343 }
344 $assoc = $class->associationMappings[$propName];
345 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
346 $changeSet[$propName] = [null, $actualValue];
347 }
348 }
349 $this->entityChangeSets[$oid] = $changeSet;
350 } else {
351 // Entity is "fully" MANAGED: it was already fully persisted before
352 // and we have a copy of the original data
353 $originalData = $this->originalEntityData[$oid];
354 $isChangeTrackingNotify = $class->isChangeTrackingNotify();
355 $changeSet = $isChangeTrackingNotify && isset($this->entityChangeSets[$oid]) ? $this->entityChangeSets[$oid] : [];
356 foreach ($actualData as $propName => $actualValue) {
357 // skip field, its a partially omitted one!
358 if (!(isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
359 continue;
360 }
361 $orgValue = $originalData[$propName];
362 if (!empty($class->fieldMappings[$propName]['enumType'])) {
363 if (is_array($orgValue)) {
364 foreach ($orgValue as $id => $val) {
365 if ($val instanceof BackedEnum) {
366 $orgValue[$id] = $val->value;
367 }
368 }
369 } else {
370 if ($orgValue instanceof BackedEnum) {
371 $orgValue = $orgValue->value;
372 }
373 }
374 }
375 // skip if value haven't changed
376 if ($orgValue === $actualValue) {
377 continue;
378 }
379 // if regular field
380 if (!isset($class->associationMappings[$propName])) {
381 if ($isChangeTrackingNotify) {
382 continue;
383 }
384 $changeSet[$propName] = [$orgValue, $actualValue];
385 continue;
386 }
387 $assoc = $class->associationMappings[$propName];
388 // Persistent collection was exchanged with the "originally"
389 // created one. This can only mean it was cloned and replaced
390 // on another entity.
391 if ($actualValue instanceof PersistentCollection) {
392 $owner = $actualValue->getOwner();
393 if ($owner === null) {
394 // cloned
395 $actualValue->setOwner($entity, $assoc);
396 } elseif ($owner !== $entity) {
397 // no clone, we have to fix
398 if (!$actualValue->isInitialized()) {
399 $actualValue->initialize();
400 // we have to do this otherwise the cols share state
401 }
402 $newValue = clone $actualValue;
403 $newValue->setOwner($entity, $assoc);
404 $class->reflFields[$propName]->setValue($entity, $newValue);
405 }
406 }
407 if ($orgValue instanceof PersistentCollection) {
408 // A PersistentCollection was de-referenced, so delete it.
409 $coid = spl_object_id($orgValue);
410 if (isset($this->collectionDeletions[$coid])) {
411 continue;
412 }
413 $this->collectionDeletions[$coid] = $orgValue;
414 $changeSet[$propName] = $orgValue;
415 // Signal changeset, to-many assocs will be ignored.
416 continue;
417 }
418 if ($assoc['type'] & ClassMetadata::TO_ONE) {
419 if ($assoc['isOwningSide']) {
420 $changeSet[$propName] = [$orgValue, $actualValue];
421 }
422 if ($orgValue !== null && $assoc['orphanRemoval']) {
423 assert(is_object($orgValue));
424 $this->scheduleOrphanRemoval($orgValue);
425 }
426 }
427 }
428 if ($changeSet) {
429 $this->entityChangeSets[$oid] = $changeSet;
430 $this->originalEntityData[$oid] = $actualData;
431 $this->entityUpdates[$oid] = $entity;
432 }
433 }
434 // Look for changes in associations of the entity
435 foreach ($class->associationMappings as $field => $assoc) {
436 $val = $class->reflFields[$field]->getValue($entity);
437 if ($val === null) {
438 continue;
439 }
440 $this->computeAssociationChanges($assoc, $val);
441 if (!isset($this->entityChangeSets[$oid]) && $assoc['isOwningSide'] && $assoc['type'] === ClassMetadata::MANY_TO_MANY && $val instanceof PersistentCollection && $val->isDirty()) {
442 $this->entityChangeSets[$oid] = [];
443 $this->originalEntityData[$oid] = $actualData;
444 $this->entityUpdates[$oid] = $entity;
445 }
446 }
447 }
448 public function computeChangeSets()
449 {
450 // Compute changes for INSERTed entities first. This must always happen.
451 $this->computeScheduleInsertsChangeSets();
452 // Compute changes for other MANAGED entities. Change tracking policies take effect here.
453 foreach ($this->identityMap as $className => $entities) {
454 $class = $this->em->getClassMetadata($className);
455 // Skip class if instances are read-only
456 if ($class->isReadOnly) {
457 continue;
458 }
459 // If change tracking is explicit or happens through notification, then only compute
460 // changes on entities of that type that are explicitly marked for synchronization.
461 switch (\true) {
462 case $class->isChangeTrackingDeferredImplicit():
463 $entitiesToProcess = $entities;
464 break;
465 case isset($this->scheduledForSynchronization[$className]):
466 $entitiesToProcess = $this->scheduledForSynchronization[$className];
467 break;
468 default:
469 $entitiesToProcess = [];
470 }
471 foreach ($entitiesToProcess as $entity) {
472 // Ignore uninitialized proxy objects
473 if ($this->isUninitializedObject($entity)) {
474 continue;
475 }
476 // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
477 $oid = spl_object_id($entity);
478 if (!isset($this->entityInsertions[$oid]) && !isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
479 $this->computeChangeSet($class, $entity);
480 }
481 }
482 }
483 }
484 private function computeAssociationChanges(array $assoc, $value) : void
485 {
486 if ($this->isUninitializedObject($value)) {
487 return;
488 }
489 // If this collection is dirty, schedule it for updates
490 if ($value instanceof PersistentCollection && $value->isDirty()) {
491 $coid = spl_object_id($value);
492 $this->collectionUpdates[$coid] = $value;
493 $this->visitedCollections[$coid] = $value;
494 }
495 // Look through the entities, and in any of their associations,
496 // for transient (new) entities, recursively. ("Persistence by reachability")
497 // Unwrap. Uninitialized collections will simply be empty.
498 $unwrappedValue = $assoc['type'] & ClassMetadata::TO_ONE ? [$value] : $value->unwrap();
499 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
500 foreach ($unwrappedValue as $key => $entry) {
501 if (!$entry instanceof $targetClass->name) {
502 throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry);
503 }
504 $state = $this->getEntityState($entry, self::STATE_NEW);
505 if (!$entry instanceof $assoc['targetEntity']) {
506 throw UnexpectedAssociationValue::create($assoc['sourceEntity'], $assoc['fieldName'], get_debug_type($entry), $assoc['targetEntity']);
507 }
508 switch ($state) {
509 case self::STATE_NEW:
510 if (!$assoc['isCascadePersist']) {
511 $this->nonCascadedNewDetectedEntities[spl_object_id($entry)] = [$assoc, $entry];
512 break;
513 }
514 $this->persistNew($targetClass, $entry);
515 $this->computeChangeSet($targetClass, $entry);
516 break;
517 case self::STATE_REMOVED:
518 // Consume the $value as array (it's either an array or an ArrayAccess)
519 // and remove the element from Collection.
520 if (!($assoc['type'] & ClassMetadata::TO_MANY)) {
521 break;
522 }
523 $coid = spl_object_id($value);
524 $this->visitedCollections[$coid] = $value;
525 if (!isset($this->pendingCollectionElementRemovals[$coid])) {
526 $this->pendingCollectionElementRemovals[$coid] = [];
527 }
528 $this->pendingCollectionElementRemovals[$coid][$key] = \true;
529 break;
530 case self::STATE_DETACHED:
531 // Can actually not happen right now as we assume STATE_NEW,
532 // so the exception will be raised from the DBAL layer (constraint violation).
533 throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry);
534 default:
535 }
536 }
537 }
538 private function persistNew(ClassMetadata $class, $entity) : void
539 {
540 $oid = spl_object_id($entity);
541 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
542 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
543 $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new PrePersistEventArgs($entity, $this->em), $invoke);
544 }
545 $idGen = $class->idGenerator;
546 if (!$idGen->isPostInsertGenerator()) {
547 $idValue = $idGen->generateId($this->em, $entity);
548 if (!$idGen instanceof AssignedGenerator) {
549 $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)];
550 $class->setIdentifierValues($entity, $idValue);
551 }
552 // Some identifiers may be foreign keys to new entities.
553 // In this case, we don't have the value yet and should treat it as if we have a post-insert generator
554 if (!$this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
555 $this->entityIdentifiers[$oid] = $idValue;
556 }
557 }
558 $this->entityStates[$oid] = self::STATE_MANAGED;
559 if (!isset($this->entityInsertions[$oid])) {
560 $this->scheduleForInsert($entity);
561 }
562 }
563 private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
564 {
565 foreach ($idValue as $idField => $idFieldValue) {
566 if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
567 return \true;
568 }
569 }
570 return \false;
571 }
572 public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
573 {
574 $oid = spl_object_id($entity);
575 if (!isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
576 throw ORMInvalidArgumentException::entityNotManaged($entity);
577 }
578 // skip if change tracking is "NOTIFY"
579 if ($class->isChangeTrackingNotify()) {
580 return;
581 }
582 if (!$class->isInheritanceTypeNone()) {
583 $class = $this->em->getClassMetadata(get_class($entity));
584 }
585 $actualData = [];
586 foreach ($class->reflFields as $name => $refProp) {
587 if ((!$class->isIdentifier($name) || !$class->isIdGeneratorIdentity()) && $name !== $class->versionField && !$class->isCollectionValuedAssociation($name)) {
588 $actualData[$name] = $refProp->getValue($entity);
589 }
590 }
591 if (!isset($this->originalEntityData[$oid])) {
592 throw new RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
593 }
594 $originalData = $this->originalEntityData[$oid];
595 $changeSet = [];
596 foreach ($actualData as $propName => $actualValue) {
597 $orgValue = $originalData[$propName] ?? null;
598 if (isset($class->fieldMappings[$propName]['enumType'])) {
599 if (is_array($orgValue)) {
600 foreach ($orgValue as $id => $val) {
601 if ($val instanceof BackedEnum) {
602 $orgValue[$id] = $val->value;
603 }
604 }
605 } else {
606 if ($orgValue instanceof BackedEnum) {
607 $orgValue = $orgValue->value;
608 }
609 }
610 }
611 if ($orgValue !== $actualValue) {
612 $changeSet[$propName] = [$orgValue, $actualValue];
613 }
614 }
615 if ($changeSet) {
616 if (isset($this->entityChangeSets[$oid])) {
617 $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
618 } elseif (!isset($this->entityInsertions[$oid])) {
619 $this->entityChangeSets[$oid] = $changeSet;
620 $this->entityUpdates[$oid] = $entity;
621 }
622 $this->originalEntityData[$oid] = $actualData;
623 }
624 }
625 private function executeInserts() : void
626 {
627 $entities = $this->computeInsertExecutionOrder();
628 $eventsToDispatch = [];
629 foreach ($entities as $entity) {
630 $oid = spl_object_id($entity);
631 $class = $this->em->getClassMetadata(get_class($entity));
632 $persister = $this->getEntityPersister($class->name);
633 $persister->addInsert($entity);
634 unset($this->entityInsertions[$oid]);
635 $postInsertIds = $persister->executeInserts();
636 if (is_array($postInsertIds)) {
637 Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/pull/10743/', 'Returning post insert IDs from \\Doctrine\\ORM\\Persisters\\Entity\\EntityPersister::executeInserts() is deprecated and will not be supported in Doctrine ORM 3.0. Make the persister call Doctrine\\ORM\\UnitOfWork::assignPostInsertId() instead.');
638 // Persister returned post-insert IDs
639 foreach ($postInsertIds as $postInsertId) {
640 $this->assignPostInsertId($postInsertId['entity'], $postInsertId['generatedId']);
641 }
642 }
643 if (!isset($this->entityIdentifiers[$oid])) {
644 //entity was not added to identity map because some identifiers are foreign keys to new entities.
645 //add it now
646 $this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
647 }
648 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
649 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
650 $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
651 }
652 }
653 // Defer dispatching `postPersist` events to until all entities have been inserted and post-insert
654 // IDs have been assigned.
655 foreach ($eventsToDispatch as $event) {
656 $this->listenersInvoker->invoke($event['class'], Events::postPersist, $event['entity'], new PostPersistEventArgs($event['entity'], $this->em), $event['invoke']);
657 }
658 }
659 private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, int $oid, $entity) : void
660 {
661 $identifier = [];
662 foreach ($class->getIdentifierFieldNames() as $idField) {
663 $origValue = $class->getFieldValue($entity, $idField);
664 $value = null;
665 if (isset($class->associationMappings[$idField])) {
666 // NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
667 $value = $this->getSingleIdentifierValue($origValue);
668 }
669 $identifier[$idField] = $value ?? $origValue;
670 $this->originalEntityData[$oid][$idField] = $origValue;
671 }
672 $this->entityStates[$oid] = self::STATE_MANAGED;
673 $this->entityIdentifiers[$oid] = $identifier;
674 $this->addToIdentityMap($entity);
675 }
676 private function executeUpdates() : void
677 {
678 foreach ($this->entityUpdates as $oid => $entity) {
679 $class = $this->em->getClassMetadata(get_class($entity));
680 $persister = $this->getEntityPersister($class->name);
681 $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
682 $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
683 if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
684 $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
685 $this->recomputeSingleEntityChangeSet($class, $entity);
686 }
687 if (!empty($this->entityChangeSets[$oid])) {
688 $persister->update($entity);
689 }
690 unset($this->entityUpdates[$oid]);
691 if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
692 $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new PostUpdateEventArgs($entity, $this->em), $postUpdateInvoke);
693 }
694 }
695 }
696 private function executeDeletions() : void
697 {
698 $entities = $this->computeDeleteExecutionOrder();
699 $eventsToDispatch = [];
700 foreach ($entities as $entity) {
701 $this->removeFromIdentityMap($entity);
702 $oid = spl_object_id($entity);
703 $class = $this->em->getClassMetadata(get_class($entity));
704 $persister = $this->getEntityPersister($class->name);
705 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
706 $persister->delete($entity);
707 unset($this->entityDeletions[$oid], $this->entityIdentifiers[$oid], $this->originalEntityData[$oid], $this->entityStates[$oid]);
708 // Entity with this $oid after deletion treated as NEW, even if the $oid
709 // is obtained by a new entity because the old one went out of scope.
710 //$this->entityStates[$oid] = self::STATE_NEW;
711 if (!$class->isIdentifierNatural()) {
712 $class->reflFields[$class->identifier[0]]->setValue($entity, null);
713 }
714 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
715 $eventsToDispatch[] = ['class' => $class, 'entity' => $entity, 'invoke' => $invoke];
716 }
717 }
718 // Defer dispatching `postRemove` events to until all entities have been removed.
719 foreach ($eventsToDispatch as $event) {
720 $this->listenersInvoker->invoke($event['class'], Events::postRemove, $event['entity'], new PostRemoveEventArgs($event['entity'], $this->em), $event['invoke']);
721 }
722 }
723 private function computeInsertExecutionOrder() : array
724 {
725 $sort = new TopologicalSort();
726 // First make sure we have all the nodes
727 foreach ($this->entityInsertions as $entity) {
728 $sort->addNode($entity);
729 }
730 // Now add edges
731 foreach ($this->entityInsertions as $entity) {
732 $class = $this->em->getClassMetadata(get_class($entity));
733 foreach ($class->associationMappings as $assoc) {
734 // We only need to consider the owning sides of to-one associations,
735 // since many-to-many associations are persisted at a later step and
736 // have no insertion order problems (all entities already in the database
737 // at that time).
738 if (!($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
739 continue;
740 }
741 $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
742 // If there is no entity that we need to refer to, or it is already in the
743 // database (i. e. does not have to be inserted), no need to consider it.
744 if ($targetEntity === null || !$sort->hasNode($targetEntity)) {
745 continue;
746 }
747 // An entity that references back to itself _and_ uses an application-provided ID
748 // (the "NONE" generator strategy) can be exempted from commit order computation.
749 // See https://github.com/doctrine/orm/pull/10735/ for more details on this edge case.
750 // A non-NULLable self-reference would be a cycle in the graph.
751 if ($targetEntity === $entity && $class->isIdentifierNatural()) {
752 continue;
753 }
754 // According to https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/annotations-reference.html#annref_joincolumn,
755 // the default for "nullable" is true. Unfortunately, it seems this default is not applied at the metadata driver, factory or other
756 // level, but in fact we may have an undefined 'nullable' key here, so we must assume that default here as well.
757 //
758 // Same in \MailPoetVendor\Doctrine\ORM\Tools\EntityGenerator::isAssociationIsNullable or \MailPoetVendor\Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getJoinSQLForJoinColumns,
759 // to give two examples.
760 assert(isset($assoc['joinColumns']));
761 $joinColumns = reset($assoc['joinColumns']);
762 $isNullable = !isset($joinColumns['nullable']) || $joinColumns['nullable'];
763 // Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
764 // topological sort result will output the depended-upon nodes first, which means we can insert
765 // entities in that order.
766 $sort->addEdge($entity, $targetEntity, $isNullable);
767 }
768 }
769 return $sort->sort();
770 }
771 private function computeDeleteExecutionOrder() : array
772 {
773 $stronglyConnectedComponents = new StronglyConnectedComponents();
774 $sort = new TopologicalSort();
775 foreach ($this->entityDeletions as $entity) {
776 $stronglyConnectedComponents->addNode($entity);
777 $sort->addNode($entity);
778 }
779 // First, consider only "on delete cascade" associations between entities
780 // and find strongly connected groups. Once we delete any one of the entities
781 // in such a group, _all_ of the other entities will be removed as well. So,
782 // we need to treat those groups like a single entity when performing delete
783 // order topological sorting.
784 foreach ($this->entityDeletions as $entity) {
785 $class = $this->em->getClassMetadata(get_class($entity));
786 foreach ($class->associationMappings as $assoc) {
787 // We only need to consider the owning sides of to-one associations,
788 // since many-to-many associations can always be (and have already been)
789 // deleted in a preceding step.
790 if (!($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
791 continue;
792 }
793 assert(isset($assoc['joinColumns']));
794 $joinColumns = reset($assoc['joinColumns']);
795 if (!isset($joinColumns['onDelete'])) {
796 continue;
797 }
798 $onDeleteOption = strtolower($joinColumns['onDelete']);
799 if ($onDeleteOption !== 'cascade') {
800 continue;
801 }
802 $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
803 // If the association does not refer to another entity or that entity
804 // is not to be deleted, there is no ordering problem and we can
805 // skip this particular association.
806 if ($targetEntity === null || !$stronglyConnectedComponents->hasNode($targetEntity)) {
807 continue;
808 }
809 $stronglyConnectedComponents->addEdge($entity, $targetEntity);
810 }
811 }
812 $stronglyConnectedComponents->findStronglyConnectedComponents();
813 // Now do the actual topological sorting to find the delete order.
814 foreach ($this->entityDeletions as $entity) {
815 $class = $this->em->getClassMetadata(get_class($entity));
816 // Get the entities representing the SCC
817 $entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
818 // When $entity is part of a non-trivial strongly connected component group
819 // (a group containing not only those entities alone), make sure we process it _after_ the
820 // entity representing the group.
821 // The dependency direction implies that "$entity depends on $entityComponent
822 // being deleted first". The topological sort will output the depended-upon nodes first.
823 if ($entityComponent !== $entity) {
824 $sort->addEdge($entity, $entityComponent, \false);
825 }
826 foreach ($class->associationMappings as $assoc) {
827 // We only need to consider the owning sides of to-one associations,
828 // since many-to-many associations can always be (and have already been)
829 // deleted in a preceding step.
830 if (!($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
831 continue;
832 }
833 // For associations that implement a database-level set null operation,
834 // we do not have to follow a particular order: If the referred-to entity is
835 // deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
836 // So, we can skip it in the computation.
837 assert(isset($assoc['joinColumns']));
838 $joinColumns = reset($assoc['joinColumns']);
839 if (isset($joinColumns['onDelete'])) {
840 $onDeleteOption = strtolower($joinColumns['onDelete']);
841 if ($onDeleteOption === 'set null') {
842 continue;
843 }
844 }
845 $targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
846 // If the association does not refer to another entity or that entity
847 // is not to be deleted, there is no ordering problem and we can
848 // skip this particular association.
849 if ($targetEntity === null || !$sort->hasNode($targetEntity)) {
850 continue;
851 }
852 // Get the entities representing the SCC
853 $targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
854 // When we have a dependency between two different groups of strongly connected nodes,
855 // add it to the computation.
856 // The dependency direction implies that "$targetEntityComponent depends on $entityComponent
857 // being deleted first". The topological sort will output the depended-upon nodes first,
858 // so we can work through the result in the returned order.
859 if ($targetEntityComponent !== $entityComponent) {
860 $sort->addEdge($targetEntityComponent, $entityComponent, \false);
861 }
862 }
863 }
864 return $sort->sort();
865 }
866 public function scheduleForInsert($entity)
867 {
868 $oid = spl_object_id($entity);
869 if (isset($this->entityUpdates[$oid])) {
870 throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
871 }
872 if (isset($this->entityDeletions[$oid])) {
873 throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
874 }
875 if (isset($this->originalEntityData[$oid]) && !isset($this->entityInsertions[$oid])) {
876 throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
877 }
878 if (isset($this->entityInsertions[$oid])) {
879 throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
880 }
881 $this->entityInsertions[$oid] = $entity;
882 if (isset($this->entityIdentifiers[$oid])) {
883 $this->addToIdentityMap($entity);
884 }
885 if ($entity instanceof NotifyPropertyChanged) {
886 $entity->addPropertyChangedListener($this);
887 }
888 }
889 public function isScheduledForInsert($entity)
890 {
891 return isset($this->entityInsertions[spl_object_id($entity)]);
892 }
893 public function scheduleForUpdate($entity)
894 {
895 $oid = spl_object_id($entity);
896 if (!isset($this->entityIdentifiers[$oid])) {
897 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
898 }
899 if (isset($this->entityDeletions[$oid])) {
900 throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
901 }
902 if (!isset($this->entityUpdates[$oid]) && !isset($this->entityInsertions[$oid])) {
903 $this->entityUpdates[$oid] = $entity;
904 }
905 }
906 public function scheduleExtraUpdate($entity, array $changeset)
907 {
908 $oid = spl_object_id($entity);
909 $extraUpdate = [$entity, $changeset];
910 if (isset($this->extraUpdates[$oid])) {
911 [, $changeset2] = $this->extraUpdates[$oid];
912 $extraUpdate = [$entity, $changeset + $changeset2];
913 }
914 $this->extraUpdates[$oid] = $extraUpdate;
915 }
916 public function isScheduledForUpdate($entity)
917 {
918 return isset($this->entityUpdates[spl_object_id($entity)]);
919 }
920 public function isScheduledForDirtyCheck($entity)
921 {
922 $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
923 return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
924 }
925 public function scheduleForDelete($entity)
926 {
927 $oid = spl_object_id($entity);
928 if (isset($this->entityInsertions[$oid])) {
929 if ($this->isInIdentityMap($entity)) {
930 $this->removeFromIdentityMap($entity);
931 }
932 unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
933 return;
934 // entity has not been persisted yet, so nothing more to do.
935 }
936 if (!$this->isInIdentityMap($entity)) {
937 return;
938 }
939 unset($this->entityUpdates[$oid]);
940 if (!isset($this->entityDeletions[$oid])) {
941 $this->entityDeletions[$oid] = $entity;
942 $this->entityStates[$oid] = self::STATE_REMOVED;
943 }
944 }
945 public function isScheduledForDelete($entity)
946 {
947 return isset($this->entityDeletions[spl_object_id($entity)]);
948 }
949 public function isEntityScheduled($entity)
950 {
951 $oid = spl_object_id($entity);
952 return isset($this->entityInsertions[$oid]) || isset($this->entityUpdates[$oid]) || isset($this->entityDeletions[$oid]);
953 }
954 public function addToIdentityMap($entity)
955 {
956 $classMetadata = $this->em->getClassMetadata(get_class($entity));
957 $idHash = $this->getIdHashByEntity($entity);
958 $className = $classMetadata->rootEntityName;
959 if (isset($this->identityMap[$className][$idHash])) {
960 if ($this->identityMap[$className][$idHash] !== $entity) {
961 if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) {
962 throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash);
963 }
964 Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/pull/10785', <<<'EXCEPTION'
965 While adding an entity of class %s with an ID hash of "%s" to the identity map,
966 another object of class %s was already present for the same ID. This will trigger
967 an exception in ORM 3.0.
968 IDs should uniquely map to entity object instances. This problem may occur if:
969 - you use application-provided IDs and reuse ID values;
970 - database-provided IDs are reassigned after truncating the database without
971 clearing the EntityManager;
972 - you might have been using EntityManager#getReference() to create a reference
973 for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
974 entity.
975 Otherwise, it might be an ORM-internal inconsistency, please report it.
976 To opt-in to the new exception, call
977 \Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity
978 manager's configuration.
979 EXCEPTION
980 , get_class($entity), $idHash, get_class($this->identityMap[$className][$idHash]));
981 }
982 return \false;
983 }
984 $this->identityMap[$className][$idHash] = $entity;
985 return \true;
986 }
987 public static final function getIdHashByIdentifier(array $identifier) : string
988 {
989 foreach ($identifier as $k => $value) {
990 if ($value instanceof BackedEnum) {
991 $identifier[$k] = $value->value;
992 }
993 }
994 return implode(' ', $identifier);
995 }
996 public function getIdHashByEntity($entity) : string
997 {
998 $identifier = $this->entityIdentifiers[spl_object_id($entity)];
999 if (empty($identifier) || in_array(null, $identifier, \true)) {
1000 $classMetadata = $this->em->getClassMetadata(get_class($entity));
1001 throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity);
1002 }
1003 return self::getIdHashByIdentifier($identifier);
1004 }
1005 public function getEntityState($entity, $assume = null)
1006 {
1007 $oid = spl_object_id($entity);
1008 if (isset($this->entityStates[$oid])) {
1009 return $this->entityStates[$oid];
1010 }
1011 if ($assume !== null) {
1012 return $assume;
1013 }
1014 // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1015 // Note that you can not remember the NEW or DETACHED state in _entityStates since
1016 // the UoW does not hold references to such objects and the object hash can be reused.
1017 // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1018 $class = $this->em->getClassMetadata(get_class($entity));
1019 $id = $class->getIdentifierValues($entity);
1020 if (!$id) {
1021 return self::STATE_NEW;
1022 }
1023 if ($class->containsForeignIdentifier || $class->containsEnumIdentifier) {
1024 $id = $this->identifierFlattener->flattenIdentifier($class, $id);
1025 }
1026 switch (\true) {
1027 case $class->isIdentifierNatural():
1028 // Check for a version field, if available, to avoid a db lookup.
1029 if ($class->isVersioned) {
1030 assert($class->versionField !== null);
1031 return $class->getFieldValue($entity, $class->versionField) ? self::STATE_DETACHED : self::STATE_NEW;
1032 }
1033 // Last try before db lookup: check the identity map.
1034 if ($this->tryGetById($id, $class->rootEntityName)) {
1035 return self::STATE_DETACHED;
1036 }
1037 // db lookup
1038 if ($this->getEntityPersister($class->name)->exists($entity)) {
1039 return self::STATE_DETACHED;
1040 }
1041 return self::STATE_NEW;
1042 case !$class->idGenerator->isPostInsertGenerator():
1043 // if we have a pre insert generator we can't be sure that having an id
1044 // really means that the entity exists. We have to verify this through
1045 // the last resort: a db lookup
1046 // Last try before db lookup: check the identity map.
1047 if ($this->tryGetById($id, $class->rootEntityName)) {
1048 return self::STATE_DETACHED;
1049 }
1050 // db lookup
1051 if ($this->getEntityPersister($class->name)->exists($entity)) {
1052 return self::STATE_DETACHED;
1053 }
1054 return self::STATE_NEW;
1055 default:
1056 return self::STATE_DETACHED;
1057 }
1058 }
1059 public function removeFromIdentityMap($entity)
1060 {
1061 $oid = spl_object_id($entity);
1062 $classMetadata = $this->em->getClassMetadata(get_class($entity));
1063 $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
1064 if ($idHash === '') {
1065 throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1066 }
1067 $className = $classMetadata->rootEntityName;
1068 if (isset($this->identityMap[$className][$idHash])) {
1069 unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
1070 //$this->entityStates[$oid] = self::STATE_DETACHED;
1071 return \true;
1072 }
1073 return \false;
1074 }
1075 public function getByIdHash($idHash, $rootClassName)
1076 {
1077 return $this->identityMap[$rootClassName][$idHash];
1078 }
1079 public function tryGetByIdHash($idHash, $rootClassName)
1080 {
1081 $stringIdHash = (string) $idHash;
1082 return $this->identityMap[$rootClassName][$stringIdHash] ?? \false;
1083 }
1084 public function isInIdentityMap($entity)
1085 {
1086 $oid = spl_object_id($entity);
1087 if (empty($this->entityIdentifiers[$oid])) {
1088 return \false;
1089 }
1090 $classMetadata = $this->em->getClassMetadata(get_class($entity));
1091 $idHash = self::getIdHashByIdentifier($this->entityIdentifiers[$oid]);
1092 return isset($this->identityMap[$classMetadata->rootEntityName][$idHash]);
1093 }
1094 public function containsIdHash($idHash, $rootClassName)
1095 {
1096 return isset($this->identityMap[$rootClassName][$idHash]);
1097 }
1098 public function persist($entity)
1099 {
1100 $visited = [];
1101 $this->doPersist($entity, $visited);
1102 }
1103 private function doPersist($entity, array &$visited) : void
1104 {
1105 $oid = spl_object_id($entity);
1106 if (isset($visited[$oid])) {
1107 return;
1108 // Prevent infinite recursion
1109 }
1110 $visited[$oid] = $entity;
1111 // Mark visited
1112 $class = $this->em->getClassMetadata(get_class($entity));
1113 // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1114 // If we would detect DETACHED here we would throw an exception anyway with the same
1115 // consequences (not recoverable/programming error), so just assuming NEW here
1116 // lets us avoid some database lookups for entities with natural identifiers.
1117 $entityState = $this->getEntityState($entity, self::STATE_NEW);
1118 switch ($entityState) {
1119 case self::STATE_MANAGED:
1120 // Nothing to do, except if policy is "deferred explicit"
1121 if ($class->isChangeTrackingDeferredExplicit()) {
1122 $this->scheduleForDirtyCheck($entity);
1123 }
1124 break;
1125 case self::STATE_NEW:
1126 $this->persistNew($class, $entity);
1127 break;
1128 case self::STATE_REMOVED:
1129 // Entity becomes managed again
1130 unset($this->entityDeletions[$oid]);
1131 $this->addToIdentityMap($entity);
1132 $this->entityStates[$oid] = self::STATE_MANAGED;
1133 if ($class->isChangeTrackingDeferredExplicit()) {
1134 $this->scheduleForDirtyCheck($entity);
1135 }
1136 break;
1137 case self::STATE_DETACHED:
1138 // Can actually not happen right now since we assume STATE_NEW.
1139 throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1140 default:
1141 throw new UnexpectedValueException(sprintf('Unexpected entity state: %s. %s', $entityState, self::objToStr($entity)));
1142 }
1143 $this->cascadePersist($entity, $visited);
1144 }
1145 public function remove($entity)
1146 {
1147 $visited = [];
1148 $this->doRemove($entity, $visited);
1149 }
1150 private function doRemove($entity, array &$visited) : void
1151 {
1152 $oid = spl_object_id($entity);
1153 if (isset($visited[$oid])) {
1154 return;
1155 // Prevent infinite recursion
1156 }
1157 $visited[$oid] = $entity;
1158 // mark visited
1159 // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1160 // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1161 $this->cascadeRemove($entity, $visited);
1162 $class = $this->em->getClassMetadata(get_class($entity));
1163 $entityState = $this->getEntityState($entity);
1164 switch ($entityState) {
1165 case self::STATE_NEW:
1166 case self::STATE_REMOVED:
1167 // nothing to do
1168 break;
1169 case self::STATE_MANAGED:
1170 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1171 if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1172 $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new PreRemoveEventArgs($entity, $this->em), $invoke);
1173 }
1174 $this->scheduleForDelete($entity);
1175 break;
1176 case self::STATE_DETACHED:
1177 throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1178 default:
1179 throw new UnexpectedValueException(sprintf('Unexpected entity state: %s. %s', $entityState, self::objToStr($entity)));
1180 }
1181 }
1182 public function merge($entity)
1183 {
1184 $visited = [];
1185 return $this->doMerge($entity, $visited);
1186 }
1187 private function doMerge($entity, array &$visited, $prevManagedCopy = null, ?array $assoc = null)
1188 {
1189 $oid = spl_object_id($entity);
1190 if (isset($visited[$oid])) {
1191 $managedCopy = $visited[$oid];
1192 if ($prevManagedCopy !== null) {
1193 $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1194 }
1195 return $managedCopy;
1196 }
1197 $class = $this->em->getClassMetadata(get_class($entity));
1198 // First we assume DETACHED, although it can still be NEW but we can avoid
1199 // an extra db-roundtrip this way. If it is not MANAGED but has an identity,
1200 // we need to fetch it from the db anyway in order to merge.
1201 // MANAGED entities are ignored by the merge operation.
1202 $managedCopy = $entity;
1203 if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1204 // Try to look the entity up in the identity map.
1205 $id = $class->getIdentifierValues($entity);
1206 // If there is no ID, it is actually NEW.
1207 if (!$id) {
1208 $managedCopy = $this->newInstance($class);
1209 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1210 $this->persistNew($class, $managedCopy);
1211 } else {
1212 $flatId = $class->containsForeignIdentifier || $class->containsEnumIdentifier ? $this->identifierFlattener->flattenIdentifier($class, $id) : $id;
1213 $managedCopy = $this->tryGetById($flatId, $class->rootEntityName);
1214 if ($managedCopy) {
1215 // We have the entity in-memory already, just make sure its not removed.
1216 if ($this->getEntityState($managedCopy) === self::STATE_REMOVED) {
1217 throw ORMInvalidArgumentException::entityIsRemoved($managedCopy, 'merge');
1218 }
1219 } else {
1220 // We need to fetch the managed copy in order to merge.
1221 $managedCopy = $this->em->find($class->name, $flatId);
1222 }
1223 if ($managedCopy === null) {
1224 // If the identifier is ASSIGNED, it is NEW, otherwise an error
1225 // since the managed entity was not found.
1226 if (!$class->isIdentifierNatural()) {
1227 throw EntityNotFoundException::fromClassNameAndIdentifier($class->getName(), $this->identifierFlattener->flattenIdentifier($class, $id));
1228 }
1229 $managedCopy = $this->newInstance($class);
1230 $class->setIdentifierValues($managedCopy, $id);
1231 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1232 $this->persistNew($class, $managedCopy);
1233 } else {
1234 $this->ensureVersionMatch($class, $entity, $managedCopy);
1235 $this->mergeEntityStateIntoManagedCopy($entity, $managedCopy);
1236 }
1237 }
1238 $visited[$oid] = $managedCopy;
1239 // mark visited
1240 if ($class->isChangeTrackingDeferredExplicit()) {
1241 $this->scheduleForDirtyCheck($entity);
1242 }
1243 }
1244 if ($prevManagedCopy !== null) {
1245 $this->updateAssociationWithMergedEntity($entity, $assoc, $prevManagedCopy, $managedCopy);
1246 }
1247 // Mark the managed copy visited as well
1248 $visited[spl_object_id($managedCopy)] = $managedCopy;
1249 $this->cascadeMerge($entity, $managedCopy, $visited);
1250 return $managedCopy;
1251 }
1252 private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy) : void
1253 {
1254 if (!($class->isVersioned && !$this->isUninitializedObject($managedCopy) && !$this->isUninitializedObject($entity))) {
1255 return;
1256 }
1257 assert($class->versionField !== null);
1258 $reflField = $class->reflFields[$class->versionField];
1259 $managedCopyVersion = $reflField->getValue($managedCopy);
1260 $entityVersion = $reflField->getValue($entity);
1261 // Throw exception if versions don't match.
1262 // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
1263 if ($managedCopyVersion == $entityVersion) {
1264 return;
1265 }
1266 throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion);
1267 }
1268 private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) : void
1269 {
1270 $assocField = $association['fieldName'];
1271 $prevClass = $this->em->getClassMetadata(get_class($previousManagedCopy));
1272 if ($association['type'] & ClassMetadata::TO_ONE) {
1273 $prevClass->reflFields[$assocField]->setValue($previousManagedCopy, $managedCopy);
1274 return;
1275 }
1276 $value = $prevClass->reflFields[$assocField]->getValue($previousManagedCopy);
1277 $value[] = $managedCopy;
1278 if ($association['type'] === ClassMetadata::ONE_TO_MANY) {
1279 $class = $this->em->getClassMetadata(get_class($entity));
1280 $class->reflFields[$association['mappedBy']]->setValue($managedCopy, $previousManagedCopy);
1281 }
1282 }
1283 public function detach($entity)
1284 {
1285 $visited = [];
1286 $this->doDetach($entity, $visited);
1287 }
1288 private function doDetach($entity, array &$visited, bool $noCascade = \false) : void
1289 {
1290 $oid = spl_object_id($entity);
1291 if (isset($visited[$oid])) {
1292 return;
1293 // Prevent infinite recursion
1294 }
1295 $visited[$oid] = $entity;
1296 // mark visited
1297 switch ($this->getEntityState($entity, self::STATE_DETACHED)) {
1298 case self::STATE_MANAGED:
1299 if ($this->isInIdentityMap($entity)) {
1300 $this->removeFromIdentityMap($entity);
1301 }
1302 unset($this->entityInsertions[$oid], $this->entityUpdates[$oid], $this->entityDeletions[$oid], $this->entityIdentifiers[$oid], $this->entityStates[$oid], $this->originalEntityData[$oid]);
1303 break;
1304 case self::STATE_NEW:
1305 case self::STATE_DETACHED:
1306 return;
1307 }
1308 if (!$noCascade) {
1309 $this->cascadeDetach($entity, $visited);
1310 }
1311 }
1312 public function refresh($entity)
1313 {
1314 $visited = [];
1315 $lockMode = null;
1316 if (func_num_args() > 1) {
1317 $lockMode = func_get_arg(1);
1318 }
1319 $this->doRefresh($entity, $visited, $lockMode);
1320 }
1321 private function doRefresh($entity, array &$visited, ?int $lockMode = null) : void
1322 {
1323 switch (\true) {
1324 case $lockMode === LockMode::PESSIMISTIC_READ:
1325 case $lockMode === LockMode::PESSIMISTIC_WRITE:
1326 if (!$this->em->getConnection()->isTransactionActive()) {
1327 throw TransactionRequiredException::transactionRequired();
1328 }
1329 }
1330 $oid = spl_object_id($entity);
1331 if (isset($visited[$oid])) {
1332 return;
1333 // Prevent infinite recursion
1334 }
1335 $visited[$oid] = $entity;
1336 // mark visited
1337 $class = $this->em->getClassMetadata(get_class($entity));
1338 if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1339 throw ORMInvalidArgumentException::entityNotManaged($entity);
1340 }
1341 $this->cascadeRefresh($entity, $visited, $lockMode);
1342 $this->getEntityPersister($class->name)->refresh(array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), $entity, $lockMode);
1343 }
1344 private function cascadeRefresh($entity, array &$visited, ?int $lockMode = null) : void
1345 {
1346 $class = $this->em->getClassMetadata(get_class($entity));
1347 $associationMappings = array_filter($class->associationMappings, static function ($assoc) {
1348 return $assoc['isCascadeRefresh'];
1349 });
1350 foreach ($associationMappings as $assoc) {
1351 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1352 switch (\true) {
1353 case $relatedEntities instanceof PersistentCollection:
1354 // Unwrap so that foreach() does not initialize
1355 $relatedEntities = $relatedEntities->unwrap();
1356 // break; is commented intentionally!
1357 case $relatedEntities instanceof Collection:
1358 case is_array($relatedEntities):
1359 foreach ($relatedEntities as $relatedEntity) {
1360 $this->doRefresh($relatedEntity, $visited, $lockMode);
1361 }
1362 break;
1363 case $relatedEntities !== null:
1364 $this->doRefresh($relatedEntities, $visited, $lockMode);
1365 break;
1366 default:
1367 }
1368 }
1369 }
1370 private function cascadeDetach($entity, array &$visited) : void
1371 {
1372 $class = $this->em->getClassMetadata(get_class($entity));
1373 $associationMappings = array_filter($class->associationMappings, static function ($assoc) {
1374 return $assoc['isCascadeDetach'];
1375 });
1376 foreach ($associationMappings as $assoc) {
1377 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1378 switch (\true) {
1379 case $relatedEntities instanceof PersistentCollection:
1380 // Unwrap so that foreach() does not initialize
1381 $relatedEntities = $relatedEntities->unwrap();
1382 // break; is commented intentionally!
1383 case $relatedEntities instanceof Collection:
1384 case is_array($relatedEntities):
1385 foreach ($relatedEntities as $relatedEntity) {
1386 $this->doDetach($relatedEntity, $visited);
1387 }
1388 break;
1389 case $relatedEntities !== null:
1390 $this->doDetach($relatedEntities, $visited);
1391 break;
1392 default:
1393 }
1394 }
1395 }
1396 private function cascadeMerge($entity, $managedCopy, array &$visited) : void
1397 {
1398 $class = $this->em->getClassMetadata(get_class($entity));
1399 $associationMappings = array_filter($class->associationMappings, static function ($assoc) {
1400 return $assoc['isCascadeMerge'];
1401 });
1402 foreach ($associationMappings as $assoc) {
1403 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1404 if ($relatedEntities instanceof Collection) {
1405 if ($relatedEntities === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
1406 continue;
1407 }
1408 if ($relatedEntities instanceof PersistentCollection) {
1409 // Unwrap so that foreach() does not initialize
1410 $relatedEntities = $relatedEntities->unwrap();
1411 }
1412 foreach ($relatedEntities as $relatedEntity) {
1413 $this->doMerge($relatedEntity, $visited, $managedCopy, $assoc);
1414 }
1415 } elseif ($relatedEntities !== null) {
1416 $this->doMerge($relatedEntities, $visited, $managedCopy, $assoc);
1417 }
1418 }
1419 }
1420 private function cascadePersist($entity, array &$visited) : void
1421 {
1422 if ($this->isUninitializedObject($entity)) {
1423 // nothing to do - proxy is not initialized, therefore we don't do anything with it
1424 return;
1425 }
1426 $class = $this->em->getClassMetadata(get_class($entity));
1427 $associationMappings = array_filter($class->associationMappings, static function ($assoc) {
1428 return $assoc['isCascadePersist'];
1429 });
1430 foreach ($associationMappings as $assoc) {
1431 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1432 switch (\true) {
1433 case $relatedEntities instanceof PersistentCollection:
1434 // Unwrap so that foreach() does not initialize
1435 $relatedEntities = $relatedEntities->unwrap();
1436 // break; is commented intentionally!
1437 case $relatedEntities instanceof Collection:
1438 case is_array($relatedEntities):
1439 if (($assoc['type'] & ClassMetadata::TO_MANY) <= 0) {
1440 throw ORMInvalidArgumentException::invalidAssociation($this->em->getClassMetadata($assoc['targetEntity']), $assoc, $relatedEntities);
1441 }
1442 foreach ($relatedEntities as $relatedEntity) {
1443 $this->doPersist($relatedEntity, $visited);
1444 }
1445 break;
1446 case $relatedEntities !== null:
1447 if (!$relatedEntities instanceof $assoc['targetEntity']) {
1448 throw ORMInvalidArgumentException::invalidAssociation($this->em->getClassMetadata($assoc['targetEntity']), $assoc, $relatedEntities);
1449 }
1450 $this->doPersist($relatedEntities, $visited);
1451 break;
1452 default:
1453 }
1454 }
1455 }
1456 private function cascadeRemove($entity, array &$visited) : void
1457 {
1458 $class = $this->em->getClassMetadata(get_class($entity));
1459 $associationMappings = array_filter($class->associationMappings, static function ($assoc) {
1460 return $assoc['isCascadeRemove'];
1461 });
1462 if ($associationMappings) {
1463 $this->initializeObject($entity);
1464 }
1465 $entitiesToCascade = [];
1466 foreach ($associationMappings as $assoc) {
1467 $relatedEntities = $class->reflFields[$assoc['fieldName']]->getValue($entity);
1468 switch (\true) {
1469 case $relatedEntities instanceof Collection:
1470 case is_array($relatedEntities):
1471 // If its a PersistentCollection initialization is intended! No unwrap!
1472 foreach ($relatedEntities as $relatedEntity) {
1473 $entitiesToCascade[] = $relatedEntity;
1474 }
1475 break;
1476 case $relatedEntities !== null:
1477 $entitiesToCascade[] = $relatedEntities;
1478 break;
1479 default:
1480 }
1481 }
1482 foreach ($entitiesToCascade as $relatedEntity) {
1483 $this->doRemove($relatedEntity, $visited);
1484 }
1485 }
1486 public function lock($entity, int $lockMode, $lockVersion = null) : void
1487 {
1488 if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1489 throw ORMInvalidArgumentException::entityNotManaged($entity);
1490 }
1491 $class = $this->em->getClassMetadata(get_class($entity));
1492 switch (\true) {
1493 case $lockMode === LockMode::OPTIMISTIC:
1494 if (!$class->isVersioned) {
1495 throw OptimisticLockException::notVersioned($class->name);
1496 }
1497 if ($lockVersion === null) {
1498 return;
1499 }
1500 $this->initializeObject($entity);
1501 assert($class->versionField !== null);
1502 $entityVersion = $class->reflFields[$class->versionField]->getValue($entity);
1503 // phpcs:ignore SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedNotEqualOperator
1504 if ($entityVersion != $lockVersion) {
1505 throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
1506 }
1507 break;
1508 case $lockMode === LockMode::NONE:
1509 case $lockMode === LockMode::PESSIMISTIC_READ:
1510 case $lockMode === LockMode::PESSIMISTIC_WRITE:
1511 if (!$this->em->getConnection()->isTransactionActive()) {
1512 throw TransactionRequiredException::transactionRequired();
1513 }
1514 $oid = spl_object_id($entity);
1515 $this->getEntityPersister($class->name)->lock(array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]), $lockMode);
1516 break;
1517 default:
1518 }
1519 }
1520 public function clear($entityName = null)
1521 {
1522 if ($entityName === null) {
1523 $this->identityMap = $this->entityIdentifiers = $this->originalEntityData = $this->entityChangeSets = $this->entityStates = $this->scheduledForSynchronization = $this->entityInsertions = $this->entityUpdates = $this->entityDeletions = $this->nonCascadedNewDetectedEntities = $this->collectionDeletions = $this->collectionUpdates = $this->extraUpdates = $this->readOnlyObjects = $this->pendingCollectionElementRemovals = $this->visitedCollections = $this->eagerLoadingEntities = $this->eagerLoadingCollections = $this->orphanRemovals = [];
1524 } else {
1525 Deprecation::triggerIfCalledFromOutside('doctrine/orm', 'https://github.com/doctrine/orm/issues/8460', 'Calling %s() with any arguments to clear specific entities is deprecated and will not be supported in Doctrine ORM 3.0.', __METHOD__);
1526 $this->clearIdentityMapForEntityName($entityName);
1527 $this->clearEntityInsertionsForEntityName($entityName);
1528 }
1529 if ($this->evm->hasListeners(Events::onClear)) {
1530 $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->em, $entityName));
1531 }
1532 }
1533 public function scheduleOrphanRemoval($entity)
1534 {
1535 $this->orphanRemovals[spl_object_id($entity)] = $entity;
1536 }
1537 public function cancelOrphanRemoval($entity)
1538 {
1539 unset($this->orphanRemovals[spl_object_id($entity)]);
1540 }
1541 public function scheduleCollectionDeletion(PersistentCollection $coll)
1542 {
1543 $coid = spl_object_id($coll);
1544 // TODO: if $coll is already scheduled for recreation ... what to do?
1545 // Just remove $coll from the scheduled recreations?
1546 unset($this->collectionUpdates[$coid]);
1547 $this->collectionDeletions[$coid] = $coll;
1548 }
1549 public function isCollectionScheduledForDeletion(PersistentCollection $coll)
1550 {
1551 return isset($this->collectionDeletions[spl_object_id($coll)]);
1552 }
1553 private function newInstance(ClassMetadata $class)
1554 {
1555 $entity = $class->newInstance();
1556 if ($entity instanceof ObjectManagerAware) {
1557 $entity->injectObjectManager($this->em, $class);
1558 }
1559 return $entity;
1560 }
1561 public function createEntity($className, array $data, &$hints = [])
1562 {
1563 $class = $this->em->getClassMetadata($className);
1564 $id = $this->identifierFlattener->flattenIdentifier($class, $data);
1565 $idHash = self::getIdHashByIdentifier($id);
1566 if (isset($this->identityMap[$class->rootEntityName][$idHash])) {
1567 $entity = $this->identityMap[$class->rootEntityName][$idHash];
1568 $oid = spl_object_id($entity);
1569 if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) {
1570 $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
1571 if ($unmanagedProxy !== $entity && $this->isIdentifierEquals($unmanagedProxy, $entity)) {
1572 // We will hydrate the given un-managed proxy anyway:
1573 // continue work, but consider it the entity from now on
1574 $entity = $unmanagedProxy;
1575 }
1576 }
1577 if ($this->isUninitializedObject($entity)) {
1578 $entity->__setInitialized(\true);
1579 if ($this->em->getConfiguration()->isLazyGhostObjectEnabled()) {
1580 // Initialize properties that have default values to their default value (similar to what
1581 Hydrator::hydrate($entity, (array) $class->reflClass->newInstanceWithoutConstructor());
1582 }
1583 } else {
1584 if (!isset($hints[Query::HINT_REFRESH]) || isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity) {
1585 return $entity;
1586 }
1587 }
1588 // inject ObjectManager upon refresh.
1589 if ($entity instanceof ObjectManagerAware) {
1590 $entity->injectObjectManager($this->em, $class);
1591 }
1592 $this->originalEntityData[$oid] = $data;
1593 if ($entity instanceof NotifyPropertyChanged) {
1594 $entity->addPropertyChangedListener($this);
1595 }
1596 } else {
1597 $entity = $this->newInstance($class);
1598 $oid = spl_object_id($entity);
1599 $this->registerManaged($entity, $id, $data);
1600 if (isset($hints[Query::HINT_READ_ONLY]) && $hints[Query::HINT_READ_ONLY] === \true) {
1601 $this->readOnlyObjects[$oid] = \true;
1602 }
1603 }
1604 foreach ($data as $field => $value) {
1605 if (isset($class->fieldMappings[$field])) {
1606 $class->reflFields[$field]->setValue($entity, $value);
1607 }
1608 }
1609 // Loading the entity right here, if its in the eager loading map get rid of it there.
1610 unset($this->eagerLoadingEntities[$class->rootEntityName][$idHash]);
1611 if (isset($this->eagerLoadingEntities[$class->rootEntityName]) && !$this->eagerLoadingEntities[$class->rootEntityName]) {
1612 unset($this->eagerLoadingEntities[$class->rootEntityName]);
1613 }
1614 // Properly initialize any unfetched associations, if partial objects are not allowed.
1615 if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
1616 Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/issues/8471', 'Partial Objects are deprecated (here entity %s)', $className);
1617 return $entity;
1618 }
1619 foreach ($class->associationMappings as $field => $assoc) {
1620 // Check if the association is not among the fetch-joined associations already.
1621 if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
1622 continue;
1623 }
1624 if (!isset($hints['fetchMode'][$class->name][$field])) {
1625 $hints['fetchMode'][$class->name][$field] = $assoc['fetch'];
1626 }
1627 $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
1628 switch (\true) {
1629 case $assoc['type'] & ClassMetadata::TO_ONE:
1630 if (!$assoc['isOwningSide']) {
1631 // use the given entity association
1632 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
1633 $this->originalEntityData[$oid][$field] = $data[$field];
1634 $class->reflFields[$field]->setValue($entity, $data[$field]);
1635 $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity);
1636 continue 2;
1637 }
1638 // Inverse side of x-to-one can never be lazy
1639 $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity));
1640 continue 2;
1641 }
1642 // use the entity association
1643 if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
1644 $class->reflFields[$field]->setValue($entity, $data[$field]);
1645 $this->originalEntityData[$oid][$field] = $data[$field];
1646 break;
1647 }
1648 $associatedId = [];
1649 // TODO: Is this even computed right in all cases of composite keys?
1650 foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
1651 $joinColumnValue = $data[$srcColumn] ?? null;
1652 if ($joinColumnValue !== null) {
1653 if ($joinColumnValue instanceof BackedEnum) {
1654 $joinColumnValue = $joinColumnValue->value;
1655 }
1656 if ($targetClass->containsForeignIdentifier) {
1657 $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
1658 } else {
1659 $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
1660 }
1661 } elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, \true)) {
1662 // the missing key is part of target's entity primary key
1663 $associatedId = [];
1664 break;
1665 }
1666 }
1667 if (!$associatedId) {
1668 // Foreign key is NULL
1669 $class->reflFields[$field]->setValue($entity, null);
1670 $this->originalEntityData[$oid][$field] = null;
1671 break;
1672 }
1673 // Foreign key is set
1674 // Check identity map first
1675 // FIXME: Can break easily with composite keys if join column values are in
1676 // wrong order. The correct order is the one in ClassMetadata#identifier.
1677 $relatedIdHash = self::getIdHashByIdentifier($associatedId);
1678 switch (\true) {
1679 case isset($this->identityMap[$targetClass->rootEntityName][$relatedIdHash]):
1680 $newValue = $this->identityMap[$targetClass->rootEntityName][$relatedIdHash];
1681 // If this is an uninitialized proxy, we are deferring eager loads,
1682 // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
1683 // then we can append this entity for eager loading!
1684 if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER && isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite && $this->isUninitializedObject($newValue)) {
1685 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId);
1686 }
1687 break;
1688 case $targetClass->subClasses:
1689 // If it might be a subtype, it can not be lazy. There isn't even
1690 // a way to solve this with deferred eager loading, which means putting
1691 // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
1692 $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
1693 break;
1694 default:
1695 $normalizedAssociatedId = $this->normalizeIdentifier($targetClass, $associatedId);
1696 switch (\true) {
1697 // We are negating the condition here. Other cases will assume it is valid!
1698 case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER:
1699 $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
1700 $this->registerManaged($newValue, $associatedId, []);
1701 break;
1702 // Deferred eager load only works for single identifier classes
1703 case isset($hints[self::HINT_DEFEREAGERLOAD]) && $hints[self::HINT_DEFEREAGERLOAD] && !$targetClass->isIdentifierComposite:
1704 // TODO: Is there a faster approach?
1705 $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId);
1706 $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId);
1707 $this->registerManaged($newValue, $associatedId, []);
1708 break;
1709 default:
1710 // TODO: This is very imperformant, ignore it?
1711 $newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId);
1712 break;
1713 }
1714 }
1715 $this->originalEntityData[$oid][$field] = $newValue;
1716 $class->reflFields[$field]->setValue($entity, $newValue);
1717 if ($assoc['inversedBy'] && $assoc['type'] & ClassMetadata::ONE_TO_ONE && $newValue !== null) {
1718 $inverseAssoc = $targetClass->associationMappings[$assoc['inversedBy']];
1719 $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($newValue, $entity);
1720 }
1721 break;
1722 default:
1723 // Ignore if its a cached collection
1724 if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
1725 break;
1726 }
1727 // use the given collection
1728 if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
1729 $data[$field]->setOwner($entity, $assoc);
1730 $class->reflFields[$field]->setValue($entity, $data[$field]);
1731 $this->originalEntityData[$oid][$field] = $data[$field];
1732 break;
1733 }
1734 // Inject collection
1735 $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
1736 $pColl->setOwner($entity, $assoc);
1737 $pColl->setInitialized(\false);
1738 $reflField = $class->reflFields[$field];
1739 $reflField->setValue($entity, $pColl);
1740 if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
1741 $isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
1742 if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && !$isIteration && !$targetClass->isIdentifierComposite && !isset($assoc['indexBy'])) {
1743 $this->scheduleCollectionForBatchLoading($pColl, $class);
1744 } else {
1745 $this->loadCollection($pColl);
1746 $pColl->takeSnapshot();
1747 }
1748 }
1749 $this->originalEntityData[$oid][$field] = $pColl;
1750 break;
1751 }
1752 }
1753 // defer invoking of postLoad event to hydration complete step
1754 $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
1755 return $entity;
1756 }
1757 public function triggerEagerLoads()
1758 {
1759 if (!$this->eagerLoadingEntities && !$this->eagerLoadingCollections) {
1760 return;
1761 }
1762 // avoid infinite recursion
1763 $eagerLoadingEntities = $this->eagerLoadingEntities;
1764 $this->eagerLoadingEntities = [];
1765 foreach ($eagerLoadingEntities as $entityName => $ids) {
1766 if (!$ids) {
1767 continue;
1768 }
1769 $class = $this->em->getClassMetadata($entityName);
1770 $batches = array_chunk($ids, $this->em->getConfiguration()->getEagerFetchBatchSize());
1771 foreach ($batches as $batchedIds) {
1772 $this->getEntityPersister($entityName)->loadAll(array_combine($class->identifier, [$batchedIds]));
1773 }
1774 }
1775 $eagerLoadingCollections = $this->eagerLoadingCollections;
1776 // avoid recursion
1777 $this->eagerLoadingCollections = [];
1778 foreach ($eagerLoadingCollections as $group) {
1779 $this->eagerLoadCollections($group['items'], $group['mapping']);
1780 }
1781 }
1782 private function eagerLoadCollections(array $collections, array $mapping) : void
1783 {
1784 $targetEntity = $mapping['targetEntity'];
1785 $class = $this->em->getClassMetadata($mapping['sourceEntity']);
1786 $mappedBy = $mapping['mappedBy'];
1787 $batches = array_chunk($collections, $this->em->getConfiguration()->getEagerFetchBatchSize(), \true);
1788 foreach ($batches as $collectionBatch) {
1789 $entities = [];
1790 foreach ($collectionBatch as $collection) {
1791 $entities[] = $collection->getOwner();
1792 }
1793 $found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping['orderBy'] ?? null);
1794 $targetClass = $this->em->getClassMetadata($targetEntity);
1795 $targetProperty = $targetClass->getReflectionProperty($mappedBy);
1796 foreach ($found as $targetValue) {
1797 $sourceEntity = $targetProperty->getValue($targetValue);
1798 if ($sourceEntity === null && isset($targetClass->associationMappings[$mappedBy]['joinColumns'])) {
1799 // case where the hydration $targetValue itself has not yet fully completed, for example
1800 // in case a bi-directional association is being hydrated and deferring eager loading is
1801 // not possible due to subclassing.
1802 $data = $this->getOriginalEntityData($targetValue);
1803 $id = [];
1804 foreach ($targetClass->associationMappings[$mappedBy]['joinColumns'] as $joinColumn) {
1805 $id[] = $data[$joinColumn['name']];
1806 }
1807 } else {
1808 $id = $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($sourceEntity));
1809 }
1810 $idHash = implode(' ', $id);
1811 if (isset($mapping['indexBy'])) {
1812 $indexByProperty = $targetClass->getReflectionProperty($mapping['indexBy']);
1813 $collectionBatch[$idHash]->hydrateSet($indexByProperty->getValue($targetValue), $targetValue);
1814 } else {
1815 $collectionBatch[$idHash]->add($targetValue);
1816 }
1817 }
1818 }
1819 foreach ($collections as $association) {
1820 $association->setInitialized(\true);
1821 $association->takeSnapshot();
1822 }
1823 }
1824 public function loadCollection(PersistentCollection $collection)
1825 {
1826 $assoc = $collection->getMapping();
1827 $persister = $this->getEntityPersister($assoc['targetEntity']);
1828 switch ($assoc['type']) {
1829 case ClassMetadata::ONE_TO_MANY:
1830 $persister->loadOneToManyCollection($assoc, $collection->getOwner(), $collection);
1831 break;
1832 case ClassMetadata::MANY_TO_MANY:
1833 $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection);
1834 break;
1835 }
1836 $collection->setInitialized(\true);
1837 }
1838 private function scheduleCollectionForBatchLoading(PersistentCollection $collection, ClassMetadata $sourceClass) : void
1839 {
1840 $mapping = $collection->getMapping();
1841 $name = $mapping['sourceEntity'] . '#' . $mapping['fieldName'];
1842 if (!isset($this->eagerLoadingCollections[$name])) {
1843 $this->eagerLoadingCollections[$name] = ['items' => [], 'mapping' => $mapping];
1844 }
1845 $owner = $collection->getOwner();
1846 assert($owner !== null);
1847 $id = $this->identifierFlattener->flattenIdentifier($sourceClass, $sourceClass->getIdentifierValues($owner));
1848 $idHash = implode(' ', $id);
1849 $this->eagerLoadingCollections[$name]['items'][$idHash] = $collection;
1850 }
1851 public function getIdentityMap()
1852 {
1853 return $this->identityMap;
1854 }
1855 public function getOriginalEntityData($entity)
1856 {
1857 $oid = spl_object_id($entity);
1858 return $this->originalEntityData[$oid] ?? [];
1859 }
1860 public function setOriginalEntityData($entity, array $data)
1861 {
1862 $this->originalEntityData[spl_object_id($entity)] = $data;
1863 }
1864 public function setOriginalEntityProperty($oid, $property, $value)
1865 {
1866 $this->originalEntityData[$oid][$property] = $value;
1867 }
1868 public function getEntityIdentifier($entity)
1869 {
1870 if (!isset($this->entityIdentifiers[spl_object_id($entity)])) {
1871 throw EntityNotFoundException::noIdentifierFound(get_debug_type($entity));
1872 }
1873 return $this->entityIdentifiers[spl_object_id($entity)];
1874 }
1875 public function getSingleIdentifierValue($entity)
1876 {
1877 $class = $this->em->getClassMetadata(get_class($entity));
1878 if ($class->isIdentifierComposite) {
1879 throw ORMInvalidArgumentException::invalidCompositeIdentifier();
1880 }
1881 $values = $this->isInIdentityMap($entity) ? $this->getEntityIdentifier($entity) : $class->getIdentifierValues($entity);
1882 return $values[$class->identifier[0]] ?? null;
1883 }
1884 public function tryGetById($id, $rootClassName)
1885 {
1886 $idHash = self::getIdHashByIdentifier((array) $id);
1887 return $this->identityMap[$rootClassName][$idHash] ?? \false;
1888 }
1889 public function scheduleForDirtyCheck($entity)
1890 {
1891 $rootClassName = $this->em->getClassMetadata(get_class($entity))->rootEntityName;
1892 $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
1893 }
1894 public function hasPendingInsertions()
1895 {
1896 return !empty($this->entityInsertions);
1897 }
1898 public function size()
1899 {
1900 return array_sum(array_map('count', $this->identityMap));
1901 }
1902 public function getEntityPersister($entityName)
1903 {
1904 if (isset($this->persisters[$entityName])) {
1905 return $this->persisters[$entityName];
1906 }
1907 $class = $this->em->getClassMetadata($entityName);
1908 switch (\true) {
1909 case $class->isInheritanceTypeNone():
1910 $persister = new BasicEntityPersister($this->em, $class);
1911 break;
1912 case $class->isInheritanceTypeSingleTable():
1913 $persister = new SingleTablePersister($this->em, $class);
1914 break;
1915 case $class->isInheritanceTypeJoined():
1916 $persister = new JoinedSubclassPersister($this->em, $class);
1917 break;
1918 default:
1919 throw new RuntimeException('No persister found for entity.');
1920 }
1921 if ($this->hasCache && $class->cache !== null) {
1922 $persister = $this->em->getConfiguration()->getSecondLevelCacheConfiguration()->getCacheFactory()->buildCachedEntityPersister($this->em, $persister, $class);
1923 }
1924 $this->persisters[$entityName] = $persister;
1925 return $this->persisters[$entityName];
1926 }
1927 public function getCollectionPersister(array $association)
1928 {
1929 $role = isset($association['cache']) ? $association['sourceEntity'] . '::' . $association['fieldName'] : $association['type'];
1930 if (isset($this->collectionPersisters[$role])) {
1931 return $this->collectionPersisters[$role];
1932 }
1933 $persister = $association['type'] === ClassMetadata::ONE_TO_MANY ? new OneToManyPersister($this->em) : new ManyToManyPersister($this->em);
1934 if ($this->hasCache && isset($association['cache'])) {
1935 $persister = $this->em->getConfiguration()->getSecondLevelCacheConfiguration()->getCacheFactory()->buildCachedCollectionPersister($this->em, $persister, $association);
1936 }
1937 $this->collectionPersisters[$role] = $persister;
1938 return $this->collectionPersisters[$role];
1939 }
1940 public function registerManaged($entity, array $id, array $data)
1941 {
1942 $oid = spl_object_id($entity);
1943 $this->entityIdentifiers[$oid] = $id;
1944 $this->entityStates[$oid] = self::STATE_MANAGED;
1945 $this->originalEntityData[$oid] = $data;
1946 $this->addToIdentityMap($entity);
1947 if ($entity instanceof NotifyPropertyChanged && !$this->isUninitializedObject($entity)) {
1948 $entity->addPropertyChangedListener($this);
1949 }
1950 }
1951 public function clearEntityChangeSet($oid)
1952 {
1953 unset($this->entityChangeSets[$oid]);
1954 }
1955 public function propertyChanged($sender, $propertyName, $oldValue, $newValue)
1956 {
1957 $oid = spl_object_id($sender);
1958 $class = $this->em->getClassMetadata(get_class($sender));
1959 $isAssocField = isset($class->associationMappings[$propertyName]);
1960 if (!$isAssocField && !isset($class->fieldMappings[$propertyName])) {
1961 return;
1962 // ignore non-persistent fields
1963 }
1964 // Update changeset and mark entity for synchronization
1965 $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
1966 if (!isset($this->scheduledForSynchronization[$class->rootEntityName][$oid])) {
1967 $this->scheduleForDirtyCheck($sender);
1968 }
1969 }
1970 public function getScheduledEntityInsertions()
1971 {
1972 return $this->entityInsertions;
1973 }
1974 public function getScheduledEntityUpdates()
1975 {
1976 return $this->entityUpdates;
1977 }
1978 public function getScheduledEntityDeletions()
1979 {
1980 return $this->entityDeletions;
1981 }
1982 public function getScheduledCollectionDeletions()
1983 {
1984 return $this->collectionDeletions;
1985 }
1986 public function getScheduledCollectionUpdates()
1987 {
1988 return $this->collectionUpdates;
1989 }
1990 public function initializeObject($obj)
1991 {
1992 if ($obj instanceof InternalProxy) {
1993 $obj->__load();
1994 return;
1995 }
1996 if ($obj instanceof PersistentCollection) {
1997 $obj->initialize();
1998 }
1999 }
2000 public function isUninitializedObject($obj) : bool
2001 {
2002 return $obj instanceof InternalProxy && !$obj->__isInitialized();
2003 }
2004 private static function objToStr($obj) : string
2005 {
2006 return method_exists($obj, '__toString') ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj);
2007 }
2008 public function markReadOnly($object)
2009 {
2010 if (!is_object($object) || !$this->isInIdentityMap($object)) {
2011 throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2012 }
2013 $this->readOnlyObjects[spl_object_id($object)] = \true;
2014 }
2015 public function isReadOnly($object)
2016 {
2017 if (!is_object($object)) {
2018 throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2019 }
2020 return isset($this->readOnlyObjects[spl_object_id($object)]);
2021 }
2022 private function afterTransactionComplete() : void
2023 {
2024 $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
2025 $persister->afterTransactionComplete();
2026 });
2027 }
2028 private function afterTransactionRolledBack() : void
2029 {
2030 $this->performCallbackOnCachedPersister(static function (CachedPersister $persister) {
2031 $persister->afterTransactionRolledBack();
2032 });
2033 }
2034 private function performCallbackOnCachedPersister(callable $callback) : void
2035 {
2036 if (!$this->hasCache) {
2037 return;
2038 }
2039 foreach (array_merge($this->persisters, $this->collectionPersisters) as $persister) {
2040 if ($persister instanceof CachedPersister) {
2041 $callback($persister);
2042 }
2043 }
2044 }
2045 private function dispatchOnFlushEvent() : void
2046 {
2047 if ($this->evm->hasListeners(Events::onFlush)) {
2048 $this->evm->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2049 }
2050 }
2051 private function dispatchPostFlushEvent() : void
2052 {
2053 if ($this->evm->hasListeners(Events::postFlush)) {
2054 $this->evm->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2055 }
2056 }
2057 private function isIdentifierEquals($entity1, $entity2) : bool
2058 {
2059 if ($entity1 === $entity2) {
2060 return \true;
2061 }
2062 $class = $this->em->getClassMetadata(get_class($entity1));
2063 if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
2064 return \false;
2065 }
2066 $oid1 = spl_object_id($entity1);
2067 $oid2 = spl_object_id($entity2);
2068 $id1 = $this->entityIdentifiers[$oid1] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity1));
2069 $id2 = $this->entityIdentifiers[$oid2] ?? $this->identifierFlattener->flattenIdentifier($class, $class->getIdentifierValues($entity2));
2070 return $id1 === $id2 || self::getIdHashByIdentifier($id1) === self::getIdHashByIdentifier($id2);
2071 }
2072 private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2073 {
2074 $entitiesNeedingCascadePersist = array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2075 $this->nonCascadedNewDetectedEntities = [];
2076 if ($entitiesNeedingCascadePersist) {
2077 throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(array_values($entitiesNeedingCascadePersist));
2078 }
2079 }
2080 private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) : void
2081 {
2082 if ($this->isUninitializedObject($entity)) {
2083 return;
2084 }
2085 $this->initializeObject($managedCopy);
2086 $class = $this->em->getClassMetadata(get_class($entity));
2087 foreach ($this->reflectionPropertiesGetter->getProperties($class->name) as $prop) {
2088 $name = $prop->name;
2089 if (PHP_VERSION_ID < 80100) {
2090 $prop->setAccessible(\true);
2091 }
2092 if (!isset($class->associationMappings[$name])) {
2093 if (!$class->isIdentifier($name)) {
2094 $prop->setValue($managedCopy, $prop->getValue($entity));
2095 }
2096 } else {
2097 $assoc2 = $class->associationMappings[$name];
2098 if ($assoc2['type'] & ClassMetadata::TO_ONE) {
2099 $other = $prop->getValue($entity);
2100 if ($other === null) {
2101 $prop->setValue($managedCopy, null);
2102 } else {
2103 if ($this->isUninitializedObject($other)) {
2104 // do not merge fields marked lazy that have not been fetched.
2105 continue;
2106 }
2107 if (!$assoc2['isCascadeMerge']) {
2108 if ($this->getEntityState($other) === self::STATE_DETACHED) {
2109 $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']);
2110 $relatedId = $targetClass->getIdentifierValues($other);
2111 $other = $this->tryGetById($relatedId, $targetClass->name);
2112 if (!$other) {
2113 if ($targetClass->subClasses) {
2114 $other = $this->em->find($targetClass->name, $relatedId);
2115 } else {
2116 $other = $this->em->getProxyFactory()->getProxy($assoc2['targetEntity'], $relatedId);
2117 $this->registerManaged($other, $relatedId, []);
2118 }
2119 }
2120 }
2121 $prop->setValue($managedCopy, $other);
2122 }
2123 }
2124 } else {
2125 $mergeCol = $prop->getValue($entity);
2126 if ($mergeCol instanceof PersistentCollection && !$mergeCol->isInitialized()) {
2127 // do not merge fields marked lazy that have not been fetched.
2128 // keep the lazy persistent collection of the managed copy.
2129 continue;
2130 }
2131 $managedCol = $prop->getValue($managedCopy);
2132 if (!$managedCol) {
2133 $managedCol = new PersistentCollection($this->em, $this->em->getClassMetadata($assoc2['targetEntity']), new ArrayCollection());
2134 $managedCol->setOwner($managedCopy, $assoc2);
2135 $prop->setValue($managedCopy, $managedCol);
2136 }
2137 if ($assoc2['isCascadeMerge']) {
2138 $managedCol->initialize();
2139 // clear and set dirty a managed collection if its not also the same collection to merge from.
2140 if (!$managedCol->isEmpty() && $managedCol !== $mergeCol) {
2141 $managedCol->unwrap()->clear();
2142 $managedCol->setDirty(\true);
2143 if ($assoc2['isOwningSide'] && $assoc2['type'] === ClassMetadata::MANY_TO_MANY && $class->isChangeTrackingNotify()) {
2144 $this->scheduleForDirtyCheck($managedCopy);
2145 }
2146 }
2147 }
2148 }
2149 }
2150 if ($class->isChangeTrackingNotify()) {
2151 // Just treat all properties as changed, there is no other choice.
2152 $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2153 }
2154 }
2155 }
2156 public function hydrationComplete()
2157 {
2158 $this->hydrationCompleteHandler->hydrationComplete();
2159 }
2160 private function clearIdentityMapForEntityName(string $entityName) : void
2161 {
2162 if (!isset($this->identityMap[$entityName])) {
2163 return;
2164 }
2165 $visited = [];
2166 foreach ($this->identityMap[$entityName] as $entity) {
2167 $this->doDetach($entity, $visited, \false);
2168 }
2169 }
2170 private function clearEntityInsertionsForEntityName(string $entityName) : void
2171 {
2172 foreach ($this->entityInsertions as $hash => $entity) {
2173 // note: performance optimization - `instanceof` is much faster than a function call
2174 if ($entity instanceof $entityName && get_class($entity) === $entityName) {
2175 unset($this->entityInsertions[$hash]);
2176 }
2177 }
2178 }
2179 private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue)
2180 {
2181 return $this->em->getConnection()->convertToPHPValue($identifierValue, $class->getTypeOfField($class->getSingleIdentifierFieldName()));
2182 }
2183 private function normalizeIdentifier(ClassMetadata $targetClass, array $flatIdentifier) : array
2184 {
2185 $normalizedAssociatedId = [];
2186 foreach ($targetClass->getIdentifierFieldNames() as $name) {
2187 if (!array_key_exists($name, $flatIdentifier)) {
2188 continue;
2189 }
2190 if (!$targetClass->isSingleValuedAssociation($name)) {
2191 $normalizedAssociatedId[$name] = $flatIdentifier[$name];
2192 continue;
2193 }
2194 $targetIdMetadata = $this->em->getClassMetadata($targetClass->getAssociationTargetClass($name));
2195 // Note: the ORM prevents using an entity with a composite identifier as an identifier association
2196 // therefore, reset($targetIdMetadata->identifier) is always correct
2197 $normalizedAssociatedId[$name] = $this->em->getReference($targetIdMetadata->getName(), $this->normalizeIdentifier($targetIdMetadata, [(string) reset($targetIdMetadata->identifier) => $flatIdentifier[$name]]));
2198 }
2199 return $normalizedAssociatedId;
2200 }
2201 public final function assignPostInsertId($entity, $generatedId) : void
2202 {
2203 $class = $this->em->getClassMetadata(get_class($entity));
2204 $idField = $class->getSingleIdentifierFieldName();
2205 $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $generatedId);
2206 $oid = spl_object_id($entity);
2207 $class->reflFields[$idField]->setValue($entity, $idValue);
2208 $this->entityIdentifiers[$oid] = [$idField => $idValue];
2209 $this->entityStates[$oid] = self::STATE_MANAGED;
2210 $this->originalEntityData[$oid][$idField] = $idValue;
2211 $this->addToIdentityMap($entity);
2212 }
2213 }
2214