Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like UnitOfWork often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use UnitOfWork, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
64 | class UnitOfWork implements PropertyChangedListener |
||
65 | { |
||
66 | /** |
||
67 | * An entity is in MANAGED state when its persistence is managed by an EntityManager. |
||
68 | */ |
||
69 | const STATE_MANAGED = 1; |
||
70 | |||
71 | /** |
||
72 | * An entity is new if it has just been instantiated (i.e. using the "new" operator) |
||
73 | * and is not (yet) managed by an EntityManager. |
||
74 | */ |
||
75 | const STATE_NEW = 2; |
||
76 | |||
77 | /** |
||
78 | * A detached entity is an instance with persistent state and identity that is not |
||
79 | * (or no longer) associated with an EntityManager (and a UnitOfWork). |
||
80 | */ |
||
81 | const STATE_DETACHED = 3; |
||
82 | |||
83 | /** |
||
84 | * A removed entity instance is an instance with a persistent identity, |
||
85 | * associated with an EntityManager, whose persistent state will be deleted |
||
86 | * on commit. |
||
87 | */ |
||
88 | const STATE_REMOVED = 4; |
||
89 | |||
90 | /** |
||
91 | * Hint used to collect all primary keys of associated entities during hydration |
||
92 | * and execute it in a dedicated query afterwards |
||
93 | * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql |
||
94 | */ |
||
95 | const HINT_DEFEREAGERLOAD = 'deferEagerLoad'; |
||
96 | |||
97 | /** |
||
98 | * The identity map that holds references to all managed entities that have |
||
99 | * an identity. The entities are grouped by their class name. |
||
100 | * Since all classes in a hierarchy must share the same identifier set, |
||
101 | * we always take the root class name of the hierarchy. |
||
102 | * |
||
103 | * @var array |
||
104 | */ |
||
105 | private $identityMap = []; |
||
106 | |||
107 | /** |
||
108 | * Map of all identifiers of managed entities. |
||
109 | * Keys are object ids (spl_object_hash). |
||
110 | * |
||
111 | * @var array |
||
112 | */ |
||
113 | private $entityIdentifiers = []; |
||
114 | |||
115 | /** |
||
116 | * Map of the original entity data of managed entities. |
||
117 | * Keys are object ids (spl_object_hash). This is used for calculating changesets |
||
118 | * at commit time. |
||
119 | * |
||
120 | * Internal note: Note that PHPs "copy-on-write" behavior helps a lot with memory usage. |
||
121 | * A value will only really be copied if the value in the entity is modified |
||
122 | * by the user. |
||
123 | * |
||
124 | * @var array |
||
125 | */ |
||
126 | private $originalEntityData = []; |
||
127 | |||
128 | /** |
||
129 | * Map of entity changes. Keys are object ids (spl_object_hash). |
||
130 | * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end. |
||
131 | * |
||
132 | * @var array |
||
133 | */ |
||
134 | private $entityChangeSets = []; |
||
135 | |||
136 | /** |
||
137 | * The (cached) states of any known entities. |
||
138 | * Keys are object ids (spl_object_hash). |
||
139 | * |
||
140 | * @var array |
||
141 | */ |
||
142 | private $entityStates = []; |
||
143 | |||
144 | /** |
||
145 | * Map of entities that are scheduled for dirty checking at commit time. |
||
146 | * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT. |
||
147 | * Keys are object ids (spl_object_hash). |
||
148 | * |
||
149 | * @var array |
||
150 | */ |
||
151 | private $scheduledForSynchronization = []; |
||
152 | |||
153 | /** |
||
154 | * A list of all pending entity insertions. |
||
155 | * |
||
156 | * @var array |
||
157 | */ |
||
158 | private $entityInsertions = []; |
||
159 | |||
160 | /** |
||
161 | * A list of all pending entity updates. |
||
162 | * |
||
163 | * @var array |
||
164 | */ |
||
165 | private $entityUpdates = []; |
||
166 | |||
167 | /** |
||
168 | * Any pending extra updates that have been scheduled by persisters. |
||
169 | * |
||
170 | * @var array |
||
171 | */ |
||
172 | private $extraUpdates = []; |
||
173 | |||
174 | /** |
||
175 | * A list of all pending entity deletions. |
||
176 | * |
||
177 | * @var array |
||
178 | */ |
||
179 | private $entityDeletions = []; |
||
180 | |||
181 | /** |
||
182 | * New entities that were discovered through relationships that were not |
||
183 | * marked as cascade-persist. During flush, this array is populated and |
||
184 | * then pruned of any entities that were discovered through a valid |
||
185 | * cascade-persist path. (Leftovers cause an error.) |
||
186 | * |
||
187 | * Keys are OIDs, payload is a two-item array describing the association |
||
188 | * and the entity. |
||
189 | * |
||
190 | * @var object[][]|array[][] indexed by respective object spl_object_hash() |
||
191 | */ |
||
192 | private $nonCascadedNewDetectedEntities = []; |
||
193 | |||
194 | /** |
||
195 | * All pending collection deletions. |
||
196 | * |
||
197 | * @var array |
||
198 | */ |
||
199 | private $collectionDeletions = []; |
||
200 | |||
201 | /** |
||
202 | * All pending collection updates. |
||
203 | * |
||
204 | * @var array |
||
205 | */ |
||
206 | private $collectionUpdates = []; |
||
207 | |||
208 | /** |
||
209 | * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork. |
||
210 | * At the end of the UnitOfWork all these collections will make new snapshots |
||
211 | * of their data. |
||
212 | * |
||
213 | * @var array |
||
214 | */ |
||
215 | private $visitedCollections = []; |
||
216 | |||
217 | /** |
||
218 | * The EntityManager that "owns" this UnitOfWork instance. |
||
219 | * |
||
220 | * @var EntityManagerInterface |
||
221 | */ |
||
222 | private $em; |
||
223 | |||
224 | /** |
||
225 | * The entity persister instances used to persist entity instances. |
||
226 | * |
||
227 | * @var array |
||
228 | */ |
||
229 | private $persisters = []; |
||
230 | |||
231 | /** |
||
232 | * The collection persister instances used to persist collections. |
||
233 | * |
||
234 | * @var array |
||
235 | */ |
||
236 | private $collectionPersisters = []; |
||
237 | |||
238 | /** |
||
239 | * The EventManager used for dispatching events. |
||
240 | * |
||
241 | * @var \Doctrine\Common\EventManager |
||
242 | */ |
||
243 | private $evm; |
||
244 | |||
245 | /** |
||
246 | * The ListenersInvoker used for dispatching events. |
||
247 | * |
||
248 | * @var \Doctrine\ORM\Event\ListenersInvoker |
||
249 | */ |
||
250 | private $listenersInvoker; |
||
251 | |||
252 | /** |
||
253 | * The IdentifierFlattener used for manipulating identifiers |
||
254 | * |
||
255 | * @var \Doctrine\ORM\Utility\IdentifierFlattener |
||
256 | */ |
||
257 | private $identifierFlattener; |
||
258 | |||
259 | /** |
||
260 | * Orphaned entities that are scheduled for removal. |
||
261 | * |
||
262 | * @var array |
||
263 | */ |
||
264 | private $orphanRemovals = []; |
||
265 | |||
266 | /** |
||
267 | * Read-Only objects are never evaluated |
||
268 | * |
||
269 | * @var array |
||
270 | */ |
||
271 | private $readOnlyObjects = []; |
||
272 | |||
273 | /** |
||
274 | * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested. |
||
275 | * |
||
276 | * @var array |
||
277 | */ |
||
278 | private $eagerLoadingEntities = []; |
||
279 | |||
280 | /** |
||
281 | * @var boolean |
||
282 | */ |
||
283 | protected $hasCache = false; |
||
284 | |||
285 | /** |
||
286 | * Helper for handling completion of hydration |
||
287 | * |
||
288 | * @var HydrationCompleteHandler |
||
289 | */ |
||
290 | private $hydrationCompleteHandler; |
||
291 | |||
292 | /** |
||
293 | * @var ReflectionPropertiesGetter |
||
294 | */ |
||
295 | private $reflectionPropertiesGetter; |
||
296 | |||
297 | /** |
||
298 | * Initializes a new UnitOfWork instance, bound to the given EntityManager. |
||
299 | * |
||
300 | * @param EntityManagerInterface $em |
||
301 | */ |
||
302 | 2411 | public function __construct(EntityManagerInterface $em) |
|
303 | { |
||
304 | 2411 | $this->em = $em; |
|
305 | 2411 | $this->evm = $em->getEventManager(); |
|
306 | 2411 | $this->listenersInvoker = new ListenersInvoker($em); |
|
307 | 2411 | $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); |
|
308 | 2411 | $this->identifierFlattener = new IdentifierFlattener($this, $em->getMetadataFactory()); |
|
309 | 2411 | $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em); |
|
310 | 2411 | $this->reflectionPropertiesGetter = new ReflectionPropertiesGetter(new RuntimeReflectionService()); |
|
311 | 2411 | } |
|
312 | |||
313 | /** |
||
314 | * Commits the UnitOfWork, executing all operations that have been postponed |
||
315 | * up to this point. The state of all managed entities will be synchronized with |
||
316 | * the database. |
||
317 | * |
||
318 | * The operations are executed in the following order: |
||
319 | * |
||
320 | * 1) All entity insertions |
||
321 | * 2) All entity updates |
||
322 | * 3) All collection deletions |
||
323 | * 4) All collection updates |
||
324 | * 5) All entity deletions |
||
325 | * |
||
326 | * @param null|object|array $entity |
||
327 | * |
||
328 | * @return void |
||
329 | * |
||
330 | * @throws \Exception |
||
331 | */ |
||
332 | 1050 | public function commit($entity = null) |
|
333 | { |
||
334 | // Raise preFlush |
||
335 | 1050 | if ($this->evm->hasListeners(Events::preFlush)) { |
|
336 | 2 | $this->evm->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em)); |
|
337 | } |
||
338 | |||
339 | // Compute changes done since last commit. |
||
340 | 1050 | if (null === $entity) { |
|
341 | 1040 | $this->computeChangeSets(); |
|
342 | 18 | } elseif (is_object($entity)) { |
|
343 | 16 | $this->computeSingleEntityChangeSet($entity); |
|
344 | 2 | } elseif (is_array($entity)) { |
|
345 | 2 | foreach ($entity as $object) { |
|
346 | 2 | $this->computeSingleEntityChangeSet($object); |
|
347 | } |
||
348 | } |
||
349 | |||
350 | 1049 | if ( ! ($this->entityInsertions || |
|
|
|||
351 | 170 | $this->entityDeletions || |
|
352 | 134 | $this->entityUpdates || |
|
353 | 41 | $this->collectionUpdates || |
|
354 | 37 | $this->collectionDeletions || |
|
355 | 1049 | $this->orphanRemovals)) { |
|
356 | 25 | $this->dispatchOnFlushEvent(); |
|
357 | 25 | $this->dispatchPostFlushEvent(); |
|
358 | |||
359 | 25 | return; // Nothing to do. |
|
360 | } |
||
361 | |||
362 | 1045 | $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations(); |
|
363 | |||
364 | 1043 | if ($this->orphanRemovals) { |
|
365 | 16 | foreach ($this->orphanRemovals as $orphan) { |
|
366 | 16 | $this->remove($orphan); |
|
367 | } |
||
368 | } |
||
369 | |||
370 | 1043 | $this->dispatchOnFlushEvent(); |
|
371 | |||
372 | // Now we need a commit order to maintain referential integrity |
||
373 | 1043 | $commitOrder = $this->getCommitOrder(); |
|
374 | |||
375 | 1043 | $conn = $this->em->getConnection(); |
|
376 | 1043 | $conn->beginTransaction(); |
|
377 | |||
378 | try { |
||
379 | // Collection deletions (deletions of complete collections) |
||
380 | 1043 | foreach ($this->collectionDeletions as $collectionToDelete) { |
|
381 | 19 | $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); |
|
382 | } |
||
383 | |||
384 | 1043 | if ($this->entityInsertions) { |
|
385 | 1039 | foreach ($commitOrder as $class) { |
|
386 | 1039 | $this->executeInserts($class); |
|
387 | } |
||
388 | } |
||
389 | |||
390 | 1042 | if ($this->entityUpdates) { |
|
391 | 119 | foreach ($commitOrder as $class) { |
|
392 | 119 | $this->executeUpdates($class); |
|
393 | } |
||
394 | } |
||
395 | |||
396 | // Extra updates that were requested by persisters. |
||
397 | 1038 | if ($this->extraUpdates) { |
|
398 | 44 | $this->executeExtraUpdates(); |
|
399 | } |
||
400 | |||
401 | // Collection updates (deleteRows, updateRows, insertRows) |
||
402 | 1038 | foreach ($this->collectionUpdates as $collectionToUpdate) { |
|
403 | 535 | $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); |
|
404 | } |
||
405 | |||
406 | // Entity deletions come last and need to be in reverse commit order |
||
407 | 1038 | if ($this->entityDeletions) { |
|
408 | 63 | for ($count = count($commitOrder), $i = $count - 1; $i >= 0 && $this->entityDeletions; --$i) { |
|
409 | 63 | $this->executeDeletions($commitOrder[$i]); |
|
410 | } |
||
411 | } |
||
412 | |||
413 | 1038 | $conn->commit(); |
|
414 | 11 | } catch (Exception $e) { |
|
415 | 11 | $this->em->close(); |
|
416 | 11 | $conn->rollBack(); |
|
417 | |||
418 | 11 | $this->afterTransactionRolledBack(); |
|
419 | |||
420 | 11 | throw $e; |
|
421 | } |
||
422 | |||
423 | 1038 | $this->afterTransactionComplete(); |
|
424 | |||
425 | // Take new snapshots from visited collections |
||
426 | 1038 | foreach ($this->visitedCollections as $coll) { |
|
427 | 534 | $coll->takeSnapshot(); |
|
428 | } |
||
429 | |||
430 | 1038 | $this->dispatchPostFlushEvent(); |
|
431 | |||
432 | 1037 | $this->postCommitCleanup($entity); |
|
433 | 1037 | } |
|
434 | |||
435 | /** |
||
436 | * @param null|object|object[] $entity |
||
437 | */ |
||
438 | 1037 | private function postCommitCleanup($entity) : void |
|
439 | { |
||
440 | 1037 | $this->entityInsertions = |
|
441 | 1037 | $this->entityUpdates = |
|
442 | 1037 | $this->entityDeletions = |
|
443 | 1037 | $this->extraUpdates = |
|
444 | 1037 | $this->collectionUpdates = |
|
445 | 1037 | $this->nonCascadedNewDetectedEntities = |
|
446 | 1037 | $this->collectionDeletions = |
|
447 | 1037 | $this->visitedCollections = |
|
448 | 1037 | $this->orphanRemovals = []; |
|
449 | |||
450 | 1037 | if (null === $entity) { |
|
451 | 1028 | $this->entityChangeSets = $this->scheduledForSynchronization = []; |
|
452 | |||
453 | 1028 | return; |
|
454 | } |
||
455 | |||
456 | 15 | $entities = \is_object($entity) |
|
457 | 13 | ? [$entity] |
|
458 | 15 | : $entity; |
|
459 | |||
460 | 15 | foreach ($entities as $object) { |
|
461 | 15 | $oid = \spl_object_hash($object); |
|
462 | |||
463 | 15 | $this->clearEntityChangeSet($oid); |
|
464 | |||
465 | 15 | unset($this->scheduledForSynchronization[$this->em->getClassMetadata(\get_class($object))->rootEntityName][$oid]); |
|
466 | } |
||
467 | 15 | } |
|
468 | |||
469 | /** |
||
470 | * Computes the changesets of all entities scheduled for insertion. |
||
471 | * |
||
472 | * @return void |
||
473 | */ |
||
474 | 1049 | private function computeScheduleInsertsChangeSets() |
|
475 | { |
||
476 | 1049 | foreach ($this->entityInsertions as $entity) { |
|
477 | 1041 | $class = $this->em->getClassMetadata(get_class($entity)); |
|
478 | |||
479 | 1041 | $this->computeChangeSet($class, $entity); |
|
480 | } |
||
481 | 1049 | } |
|
482 | |||
483 | /** |
||
484 | * Only flushes the given entity according to a ruleset that keeps the UoW consistent. |
||
485 | * |
||
486 | * 1. All entities scheduled for insertion, (orphan) removals and changes in collections are processed as well! |
||
487 | * 2. Read Only entities are skipped. |
||
488 | * 3. Proxies are skipped. |
||
489 | * 4. Only if entity is properly managed. |
||
490 | * |
||
491 | * @param object $entity |
||
492 | * |
||
493 | * @return void |
||
494 | * |
||
495 | * @throws \InvalidArgumentException |
||
496 | */ |
||
497 | 18 | private function computeSingleEntityChangeSet($entity) |
|
498 | { |
||
499 | 18 | $state = $this->getEntityState($entity); |
|
500 | |||
501 | 18 | if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { |
|
502 | 1 | throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)); |
|
503 | } |
||
504 | |||
505 | 17 | $class = $this->em->getClassMetadata(get_class($entity)); |
|
506 | |||
507 | 17 | if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) { |
|
508 | 16 | $this->persist($entity); |
|
509 | } |
||
510 | |||
511 | // Compute changes for INSERTed entities first. This must always happen even in this case. |
||
512 | 17 | $this->computeScheduleInsertsChangeSets(); |
|
513 | |||
514 | 17 | if ($class->isReadOnly) { |
|
515 | return; |
||
516 | } |
||
517 | |||
518 | // Ignore uninitialized proxy objects |
||
519 | 17 | if ($entity instanceof Proxy && ! $entity->__isInitialized__) { |
|
520 | 2 | return; |
|
521 | } |
||
522 | |||
523 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
||
524 | 15 | $oid = spl_object_hash($entity); |
|
525 | |||
526 | 15 | View Code Duplication | if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { |
527 | 6 | $this->computeChangeSet($class, $entity); |
|
528 | } |
||
529 | 15 | } |
|
530 | |||
531 | /** |
||
532 | * Executes any extra updates that have been scheduled. |
||
533 | */ |
||
534 | 44 | private function executeExtraUpdates() |
|
535 | { |
||
536 | 44 | foreach ($this->extraUpdates as $oid => $update) { |
|
537 | 44 | list ($entity, $changeset) = $update; |
|
538 | |||
539 | 44 | $this->entityChangeSets[$oid] = $changeset; |
|
540 | 44 | $this->getEntityPersister(get_class($entity))->update($entity); |
|
541 | } |
||
542 | |||
543 | 44 | $this->extraUpdates = []; |
|
544 | 44 | } |
|
545 | |||
546 | /** |
||
547 | * Gets the changeset for an entity. |
||
548 | * |
||
549 | * @param object $entity |
||
550 | * |
||
551 | * @return array |
||
552 | */ |
||
553 | 1038 | public function & getEntityChangeSet($entity) |
|
554 | { |
||
555 | 1038 | $oid = spl_object_hash($entity); |
|
556 | 1038 | $data = []; |
|
557 | |||
558 | 1038 | if (!isset($this->entityChangeSets[$oid])) { |
|
559 | 3 | return $data; |
|
560 | } |
||
561 | |||
562 | 1038 | return $this->entityChangeSets[$oid]; |
|
563 | } |
||
564 | |||
565 | /** |
||
566 | * Computes the changes that happened to a single entity. |
||
567 | * |
||
568 | * Modifies/populates the following properties: |
||
569 | * |
||
570 | * {@link _originalEntityData} |
||
571 | * If the entity is NEW or MANAGED but not yet fully persisted (only has an id) |
||
572 | * then it was not fetched from the database and therefore we have no original |
||
573 | * entity data yet. All of the current entity data is stored as the original entity data. |
||
574 | * |
||
575 | * {@link _entityChangeSets} |
||
576 | * The changes detected on all properties of the entity are stored there. |
||
577 | * A change is a tuple array where the first entry is the old value and the second |
||
578 | * entry is the new value of the property. Changesets are used by persisters |
||
579 | * to INSERT/UPDATE the persistent entity state. |
||
580 | * |
||
581 | * {@link _entityUpdates} |
||
582 | * If the entity is already fully MANAGED (has been fetched from the database before) |
||
583 | * and any changes to its properties are detected, then a reference to the entity is stored |
||
584 | * there to mark it for an update. |
||
585 | * |
||
586 | * {@link _collectionDeletions} |
||
587 | * If a PersistentCollection has been de-referenced in a fully MANAGED entity, |
||
588 | * then this collection is marked for deletion. |
||
589 | * |
||
590 | * @ignore |
||
591 | * |
||
592 | * @internal Don't call from the outside. |
||
593 | * |
||
594 | * @param ClassMetadata $class The class descriptor of the entity. |
||
595 | * @param object $entity The entity for which to compute the changes. |
||
596 | * |
||
597 | * @return void |
||
598 | */ |
||
599 | 1051 | public function computeChangeSet(ClassMetadata $class, $entity) |
|
600 | { |
||
601 | 1051 | $oid = spl_object_hash($entity); |
|
602 | |||
603 | 1051 | if (isset($this->readOnlyObjects[$oid])) { |
|
604 | 2 | return; |
|
605 | } |
||
606 | |||
607 | 1051 | if ( ! $class->isInheritanceTypeNone()) { |
|
608 | 323 | $class = $this->em->getClassMetadata(get_class($entity)); |
|
609 | } |
||
610 | |||
611 | 1051 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; |
|
612 | |||
613 | 1051 | View Code Duplication | if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
614 | 138 | $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); |
|
615 | } |
||
616 | |||
617 | 1051 | $actualData = []; |
|
618 | |||
619 | 1051 | foreach ($class->reflFields as $name => $refProp) { |
|
620 | 1051 | $value = $refProp->getValue($entity); |
|
621 | |||
622 | 1051 | if ($class->isCollectionValuedAssociation($name) && $value !== null) { |
|
623 | 792 | if ($value instanceof PersistentCollection) { |
|
624 | 202 | if ($value->getOwner() === $entity) { |
|
625 | 202 | continue; |
|
626 | } |
||
627 | |||
628 | 5 | $value = new ArrayCollection($value->getValues()); |
|
629 | } |
||
630 | |||
631 | // If $value is not a Collection then use an ArrayCollection. |
||
632 | 787 | if ( ! $value instanceof Collection) { |
|
633 | 242 | $value = new ArrayCollection($value); |
|
634 | } |
||
635 | |||
636 | 787 | $assoc = $class->associationMappings[$name]; |
|
637 | |||
638 | // Inject PersistentCollection |
||
639 | 787 | $value = new PersistentCollection( |
|
640 | 787 | $this->em, $this->em->getClassMetadata($assoc['targetEntity']), $value |
|
641 | ); |
||
642 | 787 | $value->setOwner($entity, $assoc); |
|
643 | 787 | $value->setDirty( ! $value->isEmpty()); |
|
644 | |||
645 | 787 | $class->reflFields[$name]->setValue($entity, $value); |
|
646 | |||
647 | 787 | $actualData[$name] = $value; |
|
648 | |||
649 | 787 | continue; |
|
650 | } |
||
651 | |||
652 | 1051 | if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) && ($name !== $class->versionField)) { |
|
653 | 1051 | $actualData[$name] = $value; |
|
654 | } |
||
655 | } |
||
656 | |||
657 | 1051 | if ( ! isset($this->originalEntityData[$oid])) { |
|
658 | // Entity is either NEW or MANAGED but not yet fully persisted (only has an id). |
||
659 | // These result in an INSERT. |
||
660 | 1047 | $this->originalEntityData[$oid] = $actualData; |
|
661 | 1047 | $changeSet = []; |
|
662 | |||
663 | 1047 | foreach ($actualData as $propName => $actualValue) { |
|
664 | 1025 | View Code Duplication | if ( ! isset($class->associationMappings[$propName])) { |
665 | 973 | $changeSet[$propName] = [null, $actualValue]; |
|
666 | |||
667 | 973 | continue; |
|
668 | } |
||
669 | |||
670 | 915 | $assoc = $class->associationMappings[$propName]; |
|
671 | |||
672 | 915 | if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { |
|
673 | 915 | $changeSet[$propName] = [null, $actualValue]; |
|
674 | } |
||
675 | } |
||
676 | |||
677 | 1047 | $this->entityChangeSets[$oid] = $changeSet; |
|
678 | } else { |
||
679 | // Entity is "fully" MANAGED: it was already fully persisted before |
||
680 | // and we have a copy of the original data |
||
681 | 270 | $originalData = $this->originalEntityData[$oid]; |
|
682 | 270 | $isChangeTrackingNotify = $class->isChangeTrackingNotify(); |
|
683 | 270 | $changeSet = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid])) |
|
684 | ? $this->entityChangeSets[$oid] |
||
685 | 270 | : []; |
|
686 | |||
687 | 270 | foreach ($actualData as $propName => $actualValue) { |
|
688 | // skip field, its a partially omitted one! |
||
689 | 255 | if ( ! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) { |
|
690 | 8 | continue; |
|
691 | } |
||
692 | |||
693 | 255 | $orgValue = $originalData[$propName]; |
|
694 | |||
695 | // skip if value haven't changed |
||
696 | 255 | if ($orgValue === $actualValue) { |
|
697 | 239 | continue; |
|
698 | } |
||
699 | |||
700 | // if regular field |
||
701 | 115 | View Code Duplication | if ( ! isset($class->associationMappings[$propName])) { |
702 | 60 | if ($isChangeTrackingNotify) { |
|
703 | continue; |
||
704 | } |
||
705 | |||
706 | 60 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
707 | |||
708 | 60 | continue; |
|
709 | } |
||
710 | |||
711 | 59 | $assoc = $class->associationMappings[$propName]; |
|
712 | |||
713 | // Persistent collection was exchanged with the "originally" |
||
714 | // created one. This can only mean it was cloned and replaced |
||
715 | // on another entity. |
||
716 | 59 | if ($actualValue instanceof PersistentCollection) { |
|
717 | 8 | $owner = $actualValue->getOwner(); |
|
718 | 8 | if ($owner === null) { // cloned |
|
719 | $actualValue->setOwner($entity, $assoc); |
||
720 | 8 | } else if ($owner !== $entity) { // no clone, we have to fix |
|
721 | if (!$actualValue->isInitialized()) { |
||
722 | $actualValue->initialize(); // we have to do this otherwise the cols share state |
||
723 | } |
||
724 | $newValue = clone $actualValue; |
||
725 | $newValue->setOwner($entity, $assoc); |
||
726 | $class->reflFields[$propName]->setValue($entity, $newValue); |
||
727 | } |
||
728 | } |
||
729 | |||
730 | 59 | if ($orgValue instanceof PersistentCollection) { |
|
731 | // A PersistentCollection was de-referenced, so delete it. |
||
732 | 8 | $coid = spl_object_hash($orgValue); |
|
733 | |||
734 | 8 | if (isset($this->collectionDeletions[$coid])) { |
|
735 | continue; |
||
736 | } |
||
737 | |||
738 | 8 | $this->collectionDeletions[$coid] = $orgValue; |
|
739 | 8 | $changeSet[$propName] = $orgValue; // Signal changeset, to-many assocs will be ignored. |
|
740 | |||
741 | 8 | continue; |
|
742 | } |
||
743 | |||
744 | 51 | if ($assoc['type'] & ClassMetadata::TO_ONE) { |
|
745 | 50 | if ($assoc['isOwningSide']) { |
|
746 | 22 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
747 | } |
||
748 | |||
749 | 50 | if ($orgValue !== null && $assoc['orphanRemoval']) { |
|
750 | 51 | $this->scheduleOrphanRemoval($orgValue); |
|
751 | } |
||
752 | } |
||
753 | } |
||
754 | |||
755 | 270 | if ($changeSet) { |
|
756 | 88 | $this->entityChangeSets[$oid] = $changeSet; |
|
757 | 88 | $this->originalEntityData[$oid] = $actualData; |
|
758 | 88 | $this->entityUpdates[$oid] = $entity; |
|
759 | } |
||
760 | } |
||
761 | |||
762 | // Look for changes in associations of the entity |
||
763 | 1051 | foreach ($class->associationMappings as $field => $assoc) { |
|
764 | 915 | if (($val = $class->reflFields[$field]->getValue($entity)) === null) { |
|
765 | 644 | continue; |
|
766 | } |
||
767 | |||
768 | 886 | $this->computeAssociationChanges($assoc, $val); |
|
769 | |||
770 | 880 | if ( ! isset($this->entityChangeSets[$oid]) && |
|
771 | 880 | $assoc['isOwningSide'] && |
|
772 | 880 | $assoc['type'] == ClassMetadata::MANY_TO_MANY && |
|
773 | 880 | $val instanceof PersistentCollection && |
|
774 | 880 | $val->isDirty()) { |
|
775 | |||
776 | 35 | $this->entityChangeSets[$oid] = []; |
|
777 | 35 | $this->originalEntityData[$oid] = $actualData; |
|
778 | 880 | $this->entityUpdates[$oid] = $entity; |
|
779 | } |
||
780 | } |
||
781 | 1045 | } |
|
782 | |||
783 | /** |
||
784 | * Computes all the changes that have been done to entities and collections |
||
785 | * since the last commit and stores these changes in the _entityChangeSet map |
||
786 | * temporarily for access by the persisters, until the UoW commit is finished. |
||
787 | * |
||
788 | * @return void |
||
789 | */ |
||
790 | 1040 | public function computeChangeSets() |
|
791 | { |
||
792 | // Compute changes for INSERTed entities first. This must always happen. |
||
793 | 1040 | $this->computeScheduleInsertsChangeSets(); |
|
794 | |||
795 | // Compute changes for other MANAGED entities. Change tracking policies take effect here. |
||
796 | 1040 | foreach ($this->identityMap as $className => $entities) { |
|
797 | 461 | $class = $this->em->getClassMetadata($className); |
|
798 | |||
799 | // Skip class if instances are read-only |
||
800 | 461 | if ($class->isReadOnly) { |
|
801 | 1 | continue; |
|
802 | } |
||
803 | |||
804 | // If change tracking is explicit or happens through notification, then only compute |
||
805 | // changes on entities of that type that are explicitly marked for synchronization. |
||
806 | switch (true) { |
||
807 | 460 | case ($class->isChangeTrackingDeferredImplicit()): |
|
808 | 458 | $entitiesToProcess = $entities; |
|
809 | 458 | break; |
|
810 | |||
811 | 3 | case (isset($this->scheduledForSynchronization[$className])): |
|
812 | 3 | $entitiesToProcess = $this->scheduledForSynchronization[$className]; |
|
813 | 3 | break; |
|
814 | |||
815 | default: |
||
816 | 1 | $entitiesToProcess = []; |
|
817 | |||
818 | } |
||
819 | |||
820 | 460 | foreach ($entitiesToProcess as $entity) { |
|
821 | // Ignore uninitialized proxy objects |
||
822 | 440 | if ($entity instanceof Proxy && ! $entity->__isInitialized__) { |
|
823 | 36 | continue; |
|
824 | } |
||
825 | |||
826 | // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. |
||
827 | 439 | $oid = spl_object_hash($entity); |
|
828 | |||
829 | 439 | View Code Duplication | if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { |
830 | 460 | $this->computeChangeSet($class, $entity); |
|
831 | } |
||
832 | } |
||
833 | } |
||
834 | 1040 | } |
|
835 | |||
836 | /** |
||
837 | * Computes the changes of an association. |
||
838 | * |
||
839 | * @param array $assoc The association mapping. |
||
840 | * @param mixed $value The value of the association. |
||
841 | * |
||
842 | * @throws ORMInvalidArgumentException |
||
843 | * @throws ORMException |
||
844 | * |
||
845 | * @return void |
||
846 | */ |
||
847 | 886 | private function computeAssociationChanges($assoc, $value) |
|
848 | { |
||
849 | 886 | if ($value instanceof Proxy && ! $value->__isInitialized__) { |
|
850 | 29 | return; |
|
851 | } |
||
852 | |||
853 | 885 | if ($value instanceof PersistentCollection && $value->isDirty()) { |
|
854 | 537 | $coid = spl_object_hash($value); |
|
855 | |||
856 | 537 | $this->collectionUpdates[$coid] = $value; |
|
857 | 537 | $this->visitedCollections[$coid] = $value; |
|
858 | } |
||
859 | |||
860 | // Look through the entities, and in any of their associations, |
||
861 | // for transient (new) entities, recursively. ("Persistence by reachability") |
||
862 | // Unwrap. Uninitialized collections will simply be empty. |
||
863 | 885 | $unwrappedValue = ($assoc['type'] & ClassMetadata::TO_ONE) ? [$value] : $value->unwrap(); |
|
864 | 885 | $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); |
|
865 | |||
866 | 885 | foreach ($unwrappedValue as $key => $entry) { |
|
867 | 737 | if (! ($entry instanceof $targetClass->name)) { |
|
868 | 6 | throw ORMInvalidArgumentException::invalidAssociation($targetClass, $assoc, $entry); |
|
869 | } |
||
870 | |||
871 | 731 | $state = $this->getEntityState($entry, self::STATE_NEW); |
|
872 | |||
873 | 731 | if ( ! ($entry instanceof $assoc['targetEntity'])) { |
|
874 | throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']); |
||
875 | } |
||
876 | |||
877 | switch ($state) { |
||
878 | 731 | case self::STATE_NEW: |
|
879 | 42 | if ( ! $assoc['isCascadePersist']) { |
|
880 | /* |
||
881 | * For now just record the details, because this may |
||
882 | * not be an issue if we later discover another pathway |
||
883 | * through the object-graph where cascade-persistence |
||
884 | * is enabled for this object. |
||
885 | */ |
||
886 | 6 | $this->nonCascadedNewDetectedEntities[\spl_object_hash($entry)] = [$assoc, $entry]; |
|
887 | |||
888 | 6 | break; |
|
889 | } |
||
890 | |||
891 | 37 | $this->persistNew($targetClass, $entry); |
|
892 | 37 | $this->computeChangeSet($targetClass, $entry); |
|
893 | |||
894 | 37 | break; |
|
895 | |||
896 | 723 | case self::STATE_REMOVED: |
|
897 | // Consume the $value as array (it's either an array or an ArrayAccess) |
||
898 | // and remove the element from Collection. |
||
899 | 4 | if ($assoc['type'] & ClassMetadata::TO_MANY) { |
|
900 | 3 | unset($value[$key]); |
|
901 | } |
||
902 | 4 | break; |
|
903 | |||
904 | 723 | case self::STATE_DETACHED: |
|
905 | // Can actually not happen right now as we assume STATE_NEW, |
||
906 | // so the exception will be raised from the DBAL layer (constraint violation). |
||
907 | throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($assoc, $entry); |
||
908 | break; |
||
909 | |||
910 | 731 | default: |
|
911 | // MANAGED associated entities are already taken into account |
||
912 | // during changeset calculation anyway, since they are in the identity map. |
||
913 | } |
||
914 | } |
||
915 | 879 | } |
|
916 | |||
917 | /** |
||
918 | * @param \Doctrine\ORM\Mapping\ClassMetadata $class |
||
919 | * @param object $entity |
||
920 | * |
||
921 | * @return void |
||
922 | */ |
||
923 | 1070 | private function persistNew($class, $entity) |
|
924 | { |
||
925 | 1070 | $oid = spl_object_hash($entity); |
|
926 | 1070 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist); |
|
927 | |||
928 | 1070 | View Code Duplication | if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
929 | 141 | $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
|
930 | } |
||
931 | |||
932 | 1070 | $idGen = $class->idGenerator; |
|
933 | |||
934 | 1070 | if ( ! $idGen->isPostInsertGenerator()) { |
|
935 | 281 | $idValue = $idGen->generate($this->em, $entity); |
|
936 | |||
937 | 281 | if ( ! $idGen instanceof \Doctrine\ORM\Id\AssignedGenerator) { |
|
938 | 2 | $idValue = [$class->getSingleIdentifierFieldName() => $this->convertSingleFieldIdentifierToPHPValue($class, $idValue)]; |
|
939 | |||
940 | 2 | $class->setIdentifierValues($entity, $idValue); |
|
941 | } |
||
942 | |||
943 | 281 | $this->entityIdentifiers[$oid] = $idValue; |
|
944 | } |
||
945 | |||
946 | 1070 | $this->entityStates[$oid] = self::STATE_MANAGED; |
|
947 | |||
948 | 1070 | $this->scheduleForInsert($entity); |
|
949 | 1070 | } |
|
950 | |||
951 | /** |
||
952 | * INTERNAL: |
||
953 | * Computes the changeset of an individual entity, independently of the |
||
954 | * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit(). |
||
955 | * |
||
956 | * The passed entity must be a managed entity. If the entity already has a change set |
||
957 | * because this method is invoked during a commit cycle then the change sets are added. |
||
958 | * whereby changes detected in this method prevail. |
||
959 | * |
||
960 | * @ignore |
||
961 | * |
||
962 | * @param ClassMetadata $class The class descriptor of the entity. |
||
963 | * @param object $entity The entity for which to (re)calculate the change set. |
||
964 | * |
||
965 | * @return void |
||
966 | * |
||
967 | * @throws ORMInvalidArgumentException If the passed entity is not MANAGED. |
||
968 | */ |
||
969 | 16 | public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) |
|
970 | { |
||
971 | 16 | $oid = spl_object_hash($entity); |
|
972 | |||
973 | 16 | if ( ! isset($this->entityStates[$oid]) || $this->entityStates[$oid] != self::STATE_MANAGED) { |
|
974 | throw ORMInvalidArgumentException::entityNotManaged($entity); |
||
975 | } |
||
976 | |||
977 | // skip if change tracking is "NOTIFY" |
||
978 | 16 | if ($class->isChangeTrackingNotify()) { |
|
979 | return; |
||
980 | } |
||
981 | |||
982 | 16 | if ( ! $class->isInheritanceTypeNone()) { |
|
983 | 3 | $class = $this->em->getClassMetadata(get_class($entity)); |
|
984 | } |
||
985 | |||
986 | 16 | $actualData = []; |
|
987 | |||
988 | 16 | foreach ($class->reflFields as $name => $refProp) { |
|
989 | 16 | if (( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) |
|
990 | 16 | && ($name !== $class->versionField) |
|
991 | 16 | && ! $class->isCollectionValuedAssociation($name)) { |
|
992 | 16 | $actualData[$name] = $refProp->getValue($entity); |
|
993 | } |
||
994 | } |
||
995 | |||
996 | 16 | if ( ! isset($this->originalEntityData[$oid])) { |
|
997 | throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.'); |
||
998 | } |
||
999 | |||
1000 | 16 | $originalData = $this->originalEntityData[$oid]; |
|
1001 | 16 | $changeSet = []; |
|
1002 | |||
1003 | 16 | foreach ($actualData as $propName => $actualValue) { |
|
1004 | 16 | $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null; |
|
1005 | |||
1006 | 16 | if ($orgValue !== $actualValue) { |
|
1007 | 16 | $changeSet[$propName] = [$orgValue, $actualValue]; |
|
1008 | } |
||
1009 | } |
||
1010 | |||
1011 | 16 | if ($changeSet) { |
|
1012 | 7 | if (isset($this->entityChangeSets[$oid])) { |
|
1013 | 6 | $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet); |
|
1014 | 1 | } else if ( ! isset($this->entityInsertions[$oid])) { |
|
1015 | 1 | $this->entityChangeSets[$oid] = $changeSet; |
|
1016 | 1 | $this->entityUpdates[$oid] = $entity; |
|
1017 | } |
||
1018 | 7 | $this->originalEntityData[$oid] = $actualData; |
|
1019 | } |
||
1020 | 16 | } |
|
1021 | |||
1022 | /** |
||
1023 | * Executes all entity insertions for entities of the specified type. |
||
1024 | * |
||
1025 | * @param \Doctrine\ORM\Mapping\ClassMetadata $class |
||
1026 | * |
||
1027 | * @return void |
||
1028 | */ |
||
1029 | 1039 | private function executeInserts($class) |
|
1030 | { |
||
1031 | 1039 | $entities = []; |
|
1032 | 1039 | $className = $class->name; |
|
1033 | 1039 | $persister = $this->getEntityPersister($className); |
|
1034 | 1039 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); |
|
1035 | |||
1036 | 1039 | foreach ($this->entityInsertions as $oid => $entity) { |
|
1037 | |||
1038 | 1039 | if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { |
|
1039 | 881 | continue; |
|
1040 | } |
||
1041 | |||
1042 | 1039 | $persister->addInsert($entity); |
|
1043 | |||
1044 | 1039 | unset($this->entityInsertions[$oid]); |
|
1045 | |||
1046 | 1039 | if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
|
1047 | 1039 | $entities[] = $entity; |
|
1048 | } |
||
1049 | } |
||
1050 | |||
1051 | 1039 | $postInsertIds = $persister->executeInserts(); |
|
1052 | |||
1053 | 1039 | if ($postInsertIds) { |
|
1054 | // Persister returned post-insert IDs |
||
1055 | 943 | foreach ($postInsertIds as $postInsertId) { |
|
1056 | 943 | $idField = $class->getSingleIdentifierFieldName(); |
|
1057 | 943 | $idValue = $this->convertSingleFieldIdentifierToPHPValue($class, $postInsertId['generatedId']); |
|
1058 | |||
1059 | 943 | $entity = $postInsertId['entity']; |
|
1060 | 943 | $oid = spl_object_hash($entity); |
|
1061 | |||
1062 | 943 | $class->reflFields[$idField]->setValue($entity, $idValue); |
|
1063 | |||
1064 | 943 | $this->entityIdentifiers[$oid] = [$idField => $idValue]; |
|
1065 | 943 | $this->entityStates[$oid] = self::STATE_MANAGED; |
|
1066 | 943 | $this->originalEntityData[$oid][$idField] = $idValue; |
|
1067 | |||
1068 | 943 | $this->addToIdentityMap($entity); |
|
1069 | } |
||
1070 | } |
||
1071 | |||
1072 | 1039 | foreach ($entities as $entity) { |
|
1073 | 136 | $this->listenersInvoker->invoke($class, Events::postPersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
|
1074 | } |
||
1075 | 1039 | } |
|
1076 | |||
1077 | /** |
||
1078 | * Executes all entity updates for entities of the specified type. |
||
1079 | * |
||
1080 | * @param \Doctrine\ORM\Mapping\ClassMetadata $class |
||
1081 | * |
||
1082 | * @return void |
||
1083 | */ |
||
1084 | 119 | private function executeUpdates($class) |
|
1085 | { |
||
1086 | 119 | $className = $class->name; |
|
1087 | 119 | $persister = $this->getEntityPersister($className); |
|
1088 | 119 | $preUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate); |
|
1089 | 119 | $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); |
|
1090 | |||
1091 | 119 | foreach ($this->entityUpdates as $oid => $entity) { |
|
1092 | 119 | if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { |
|
1093 | 77 | continue; |
|
1094 | } |
||
1095 | |||
1096 | 119 | if ($preUpdateInvoke != ListenersInvoker::INVOKE_NONE) { |
|
1097 | 13 | $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke); |
|
1098 | |||
1099 | 13 | $this->recomputeSingleEntityChangeSet($class, $entity); |
|
1100 | } |
||
1101 | |||
1102 | 119 | if ( ! empty($this->entityChangeSets[$oid])) { |
|
1103 | 85 | $persister->update($entity); |
|
1104 | } |
||
1105 | |||
1106 | 115 | unset($this->entityUpdates[$oid]); |
|
1107 | |||
1108 | 115 | View Code Duplication | if ($postUpdateInvoke != ListenersInvoker::INVOKE_NONE) { |
1109 | 115 | $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke); |
|
1110 | } |
||
1111 | } |
||
1112 | 115 | } |
|
1113 | |||
1114 | /** |
||
1115 | * Executes all entity deletions for entities of the specified type. |
||
1116 | * |
||
1117 | * @param \Doctrine\ORM\Mapping\ClassMetadata $class |
||
1118 | * |
||
1119 | * @return void |
||
1120 | */ |
||
1121 | 63 | private function executeDeletions($class) |
|
1122 | { |
||
1123 | 63 | $className = $class->name; |
|
1124 | 63 | $persister = $this->getEntityPersister($className); |
|
1125 | 63 | $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove); |
|
1126 | |||
1127 | 63 | foreach ($this->entityDeletions as $oid => $entity) { |
|
1128 | 63 | if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { |
|
1129 | 25 | continue; |
|
1130 | } |
||
1131 | |||
1132 | 63 | $persister->delete($entity); |
|
1133 | |||
1134 | unset( |
||
1135 | 63 | $this->entityDeletions[$oid], |
|
1136 | 63 | $this->entityIdentifiers[$oid], |
|
1137 | 63 | $this->originalEntityData[$oid], |
|
1138 | 63 | $this->entityStates[$oid] |
|
1139 | ); |
||
1140 | |||
1141 | // Entity with this $oid after deletion treated as NEW, even if the $oid |
||
1142 | // is obtained by a new entity because the old one went out of scope. |
||
1143 | //$this->entityStates[$oid] = self::STATE_NEW; |
||
1144 | 63 | if ( ! $class->isIdentifierNatural()) { |
|
1145 | 53 | $class->reflFields[$class->identifier[0]]->setValue($entity, null); |
|
1146 | } |
||
1147 | |||
1148 | 63 | View Code Duplication | if ($invoke !== ListenersInvoker::INVOKE_NONE) { |
1149 | 63 | $this->listenersInvoker->invoke($class, Events::postRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke); |
|
1150 | } |
||
1151 | } |
||
1152 | 62 | } |
|
1153 | |||
1154 | /** |
||
1155 | * Gets the commit order. |
||
1156 | * |
||
1157 | * @param array|null $entityChangeSet |
||
1158 | * |
||
1159 | * @return array |
||
1160 | */ |
||
1161 | 1043 | private function getCommitOrder(array $entityChangeSet = null) |
|
1162 | { |
||
1163 | 1043 | if ($entityChangeSet === null) { |
|
1164 | 1043 | $entityChangeSet = array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions); |
|
1165 | } |
||
1166 | |||
1167 | 1043 | $calc = $this->getCommitOrderCalculator(); |
|
1168 | |||
1169 | // See if there are any new classes in the changeset, that are not in the |
||
1170 | // commit order graph yet (don't have a node). |
||
1171 | // We have to inspect changeSet to be able to correctly build dependencies. |
||
1172 | // It is not possible to use IdentityMap here because post inserted ids |
||
1173 | // are not yet available. |
||
1174 | 1043 | $newNodes = []; |
|
1175 | |||
1176 | 1043 | foreach ($entityChangeSet as $entity) { |
|
1177 | 1043 | $class = $this->em->getClassMetadata(get_class($entity)); |
|
1178 | |||
1179 | 1043 | if ($calc->hasNode($class->name)) { |
|
1180 | 637 | continue; |
|
1181 | } |
||
1182 | |||
1183 | 1043 | $calc->addNode($class->name, $class); |
|
1184 | |||
1185 | 1043 | $newNodes[] = $class; |
|
1186 | } |
||
1187 | |||
1188 | // Calculate dependencies for new nodes |
||
1189 | 1043 | while ($class = array_pop($newNodes)) { |
|
1190 | 1043 | foreach ($class->associationMappings as $assoc) { |
|
1191 | 906 | if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) { |
|
1192 | 863 | continue; |
|
1193 | } |
||
1194 | |||
1195 | 857 | $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); |
|
1196 | |||
1197 | 857 | if ( ! $calc->hasNode($targetClass->name)) { |
|
1198 | 660 | $calc->addNode($targetClass->name, $targetClass); |
|
1199 | |||
1200 | 660 | $newNodes[] = $targetClass; |
|
1201 | } |
||
1202 | |||
1203 | 857 | $joinColumns = reset($assoc['joinColumns']); |
|
1204 | |||
1205 | 857 | $calc->addDependency($targetClass->name, $class->name, (int)empty($joinColumns['nullable'])); |
|
1206 | |||
1207 | // If the target class has mapped subclasses, these share the same dependency. |
||
1208 | 857 | if ( ! $targetClass->subClasses) { |
|
1209 | 850 | continue; |
|
1210 | } |
||
1211 | |||
1212 | 226 | foreach ($targetClass->subClasses as $subClassName) { |
|
1213 | 226 | $targetSubClass = $this->em->getClassMetadata($subClassName); |
|
1214 | |||
1215 | 226 | if ( ! $calc->hasNode($subClassName)) { |
|
1216 | 196 | $calc->addNode($targetSubClass->name, $targetSubClass); |
|
1217 | |||
1218 | 196 | $newNodes[] = $targetSubClass; |
|
1219 | } |
||
1220 | |||
1221 | 226 | $calc->addDependency($targetSubClass->name, $class->name, 1); |
|
1222 | } |
||
1223 | } |
||
1224 | } |
||
1225 | |||
1226 | 1043 | return $calc->sort(); |
|
1227 | } |
||
1228 | |||
1229 | /** |
||
1230 | * Schedules an entity for insertion into the database. |
||
1231 | * If the entity already has an identifier, it will be added to the identity map. |
||
1232 | * |
||
1233 | * @param object $entity The entity to schedule for insertion. |
||
1234 | * |
||
1235 | * @return void |
||
1236 | * |
||
1237 | * @throws ORMInvalidArgumentException |
||
1238 | * @throws \InvalidArgumentException |
||
1239 | */ |
||
1240 | 1071 | public function scheduleForInsert($entity) |
|
1241 | { |
||
1242 | 1071 | $oid = spl_object_hash($entity); |
|
1243 | |||
1244 | 1071 | if (isset($this->entityUpdates[$oid])) { |
|
1245 | throw new InvalidArgumentException("Dirty entity can not be scheduled for insertion."); |
||
1246 | } |
||
1247 | |||
1248 | 1071 | if (isset($this->entityDeletions[$oid])) { |
|
1249 | 1 | throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity); |
|
1250 | } |
||
1251 | 1071 | if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) { |
|
1252 | 1 | throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity); |
|
1253 | } |
||
1254 | |||
1255 | 1071 | if (isset($this->entityInsertions[$oid])) { |
|
1256 | 1 | throw ORMInvalidArgumentException::scheduleInsertTwice($entity); |
|
1257 | } |
||
1258 | |||
1259 | 1071 | $this->entityInsertions[$oid] = $entity; |
|
1260 | |||
1261 | 1071 | if (isset($this->entityIdentifiers[$oid])) { |
|
1262 | 281 | $this->addToIdentityMap($entity); |
|
1263 | } |
||
1264 | |||
1265 | 1071 | if ($entity instanceof NotifyPropertyChanged) { |
|
1266 | 7 | $entity->addPropertyChangedListener($this); |
|
1267 | } |
||
1268 | 1071 | } |
|
1269 | |||
1270 | /** |
||
1271 | * Checks whether an entity is scheduled for insertion. |
||
1272 | * |
||
1273 | * @param object $entity |
||
1274 | * |
||
1275 | * @return boolean |
||
1276 | */ |
||
1277 | 645 | public function isScheduledForInsert($entity) |
|
1278 | { |
||
1279 | 645 | return isset($this->entityInsertions[spl_object_hash($entity)]); |
|
1280 | } |
||
1281 | |||
1282 | /** |
||
1283 | * Schedules an entity for being updated. |
||
1284 | * |
||
1285 | * @param object $entity The entity to schedule for being updated. |
||
1286 | * |
||
1287 | * @return void |
||
1288 | * |
||
1289 | * @throws ORMInvalidArgumentException |
||
1290 | */ |
||
1291 | 1 | public function scheduleForUpdate($entity) |
|
1292 | { |
||
1293 | 1 | $oid = spl_object_hash($entity); |
|
1294 | |||
1295 | 1 | if ( ! isset($this->entityIdentifiers[$oid])) { |
|
1296 | throw ORMInvalidArgumentException::entityHasNoIdentity($entity, "scheduling for update"); |
||
1297 | } |
||
1298 | |||
1299 | 1 | if (isset($this->entityDeletions[$oid])) { |
|
1300 | throw ORMInvalidArgumentException::entityIsRemoved($entity, "schedule for update"); |
||
1301 | } |
||
1302 | |||
1303 | 1 | if ( ! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) { |
|
1304 | 1 | $this->entityUpdates[$oid] = $entity; |
|
1305 | } |
||
1306 | 1 | } |
|
1307 | |||
1308 | /** |
||
1309 | * INTERNAL: |
||
1310 | * Schedules an extra update that will be executed immediately after the |
||
1311 | * regular entity updates within the currently running commit cycle. |
||
1312 | * |
||
1313 | * Extra updates for entities are stored as (entity, changeset) tuples. |
||
1314 | * |
||
1315 | * @ignore |
||
1316 | * |
||
1317 | * @param object $entity The entity for which to schedule an extra update. |
||
1318 | * @param array $changeset The changeset of the entity (what to update). |
||
1319 | * |
||
1320 | * @return void |
||
1321 | */ |
||
1322 | 44 | public function scheduleExtraUpdate($entity, array $changeset) |
|
1323 | { |
||
1324 | 44 | $oid = spl_object_hash($entity); |
|
1325 | 44 | $extraUpdate = [$entity, $changeset]; |
|
1326 | |||
1327 | 44 | if (isset($this->extraUpdates[$oid])) { |
|
1328 | 1 | list(, $changeset2) = $this->extraUpdates[$oid]; |
|
1329 | |||
1330 | 1 | $extraUpdate = [$entity, $changeset + $changeset2]; |
|
1331 | } |
||
1332 | |||
1333 | 44 | $this->extraUpdates[$oid] = $extraUpdate; |
|
1334 | 44 | } |
|
1335 | |||
1336 | /** |
||
1337 | * Checks whether an entity is registered as dirty in the unit of work. |
||
1338 | * Note: Is not very useful currently as dirty entities are only registered |
||
1339 | * at commit time. |
||
1340 | * |
||
1341 | * @param object $entity |
||
1342 | * |
||
1343 | * @return boolean |
||
1344 | */ |
||
1345 | public function isScheduledForUpdate($entity) |
||
1346 | { |
||
1347 | return isset($this->entityUpdates[spl_object_hash($entity)]); |
||
1348 | } |
||
1349 | |||
1350 | /** |
||
1351 | * Checks whether an entity is registered to be checked in the unit of work. |
||
1352 | * |
||
1353 | * @param object $entity |
||
1354 | * |
||
1355 | * @return boolean |
||
1356 | */ |
||
1357 | 1 | public function isScheduledForDirtyCheck($entity) |
|
1358 | { |
||
1359 | 1 | $rootEntityName = $this->em->getClassMetadata(get_class($entity))->rootEntityName; |
|
1360 | |||
1361 | 1 | return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_hash($entity)]); |
|
1362 | } |
||
1363 | |||
1364 | /** |
||
1365 | * INTERNAL: |
||
1366 | * Schedules an entity for deletion. |
||
1367 | * |
||
1368 | * @param object $entity |
||
1369 | * |
||
1370 | * @return void |
||
1371 | */ |
||
1372 | 66 | public function scheduleForDelete($entity) |
|
1373 | { |
||
1374 | 66 | $oid = spl_object_hash($entity); |
|
1375 | |||
1376 | 66 | if (isset($this->entityInsertions[$oid])) { |
|
1377 | 1 | if ($this->isInIdentityMap($entity)) { |
|
1378 | $this->removeFromIdentityMap($entity); |
||
1379 | } |
||
1380 | |||
1381 | 1 | unset($this->entityInsertions[$oid], $this->entityStates[$oid]); |
|
1382 | |||
1383 | 1 | return; // entity has not been persisted yet, so nothing more to do. |
|
1384 | } |
||
1385 | |||
1386 | 66 | if ( ! $this->isInIdentityMap($entity)) { |
|
1387 | 1 | return; |
|
1388 | } |
||
1389 | |||
1390 | 65 | $this->removeFromIdentityMap($entity); |
|
1391 | |||
1392 | 65 | unset($this->entityUpdates[$oid]); |
|
1393 | |||
1394 | 65 | if ( ! isset($this->entityDeletions[$oid])) { |
|
1395 | 65 | $this->entityDeletions[$oid] = $entity; |
|
1396 | 65 | $this->entityStates[$oid] = self::STATE_REMOVED; |
|
1397 | } |
||
1398 | 65 | } |
|
1399 | |||
1400 | /** |
||
1401 | * Checks whether an entity is registered as removed/deleted with the unit |
||
1402 | * of work. |
||
1403 | * |
||
1404 | * @param object $entity |
||
1405 | * |
||
1406 | * @return boolean |
||
1407 | */ |
||
1408 | 17 | public function isScheduledForDelete($entity) |
|
1412 | |||
1413 | /** |
||
1414 | * Checks whether an entity is scheduled for insertion, update or deletion. |
||
1415 | * |
||
1416 | * @param object $entity |
||
1417 | * |
||
1418 | * @return boolean |
||
1419 | */ |
||
1420 | public function isEntityScheduled($entity) |
||
1428 | |||
1429 | /** |
||
1430 | * INTERNAL: |
||
1431 | * Registers an entity in the identity map. |
||
1432 | * Note that entities in a hierarchy are registered with the class name of |
||
1433 | * the root entity. |
||
1434 | * |
||
1435 | * @ignore |
||
1436 | * |
||
1437 | * @param object $entity The entity to register. |
||
1438 | * |
||
1439 | * @return boolean TRUE if the registration was successful, FALSE if the identity of |
||
1440 | * the entity in question is already managed. |
||
1441 | * |
||
1442 | * @throws ORMInvalidArgumentException |
||
1443 | */ |
||
1444 | 1137 | public function addToIdentityMap($entity) |
|
1445 | { |
||
1446 | 1137 | $classMetadata = $this->em->getClassMetadata(get_class($entity)); |
|
1447 | 1137 | $identifier = $this->entityIdentifiers[spl_object_hash($entity)]; |
|
1448 | |||
1449 | 1137 | if (empty($identifier) || in_array(null, $identifier, true)) { |
|
1450 | 6 | throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->name, $entity); |
|
1451 | } |
||
1464 | |||
1465 | /** |
||
1466 | * Gets the state of an entity with regard to the current unit of work. |
||
1467 | * |
||
1468 | * @param object $entity |
||
1469 | * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED). |
||
1470 | * This parameter can be set to improve performance of entity state detection |
||
1471 | * by potentially avoiding a database lookup if the distinction between NEW and DETACHED |
||
1472 | * is either known or does not matter for the caller of the method. |
||
1473 | * |
||
1474 | * @return int The entity state. |
||
1475 | */ |
||
1476 | 1085 | public function getEntityState($entity, $assume = null) |
|
1545 | |||
1546 | /** |
||
1547 | * INTERNAL: |
||
1548 | * Removes an entity from the identity map. This effectively detaches the |
||
1549 | * entity from the persistence management of Doctrine. |
||
1550 | * |
||
1551 | * @ignore |
||
1552 | * |
||
1553 | * @param object $entity |
||
1554 | * |
||
1555 | * @return boolean |
||
1556 | * |
||
1557 | * @throws ORMInvalidArgumentException |
||
1558 | */ |
||
1559 | 78 | public function removeFromIdentityMap($entity) |
|
1582 | |||
1583 | /** |
||
1584 | * INTERNAL: |
||
1585 | * Gets an entity in the identity map by its identifier hash. |
||
1586 | * |
||
1587 | * @ignore |
||
1588 | * |
||
1589 | * @param string $idHash |
||
1590 | * @param string $rootClassName |
||
1591 | * |
||
1592 | * @return object |
||
1593 | */ |
||
1594 | 6 | public function getByIdHash($idHash, $rootClassName) |
|
1598 | |||
1599 | /** |
||
1600 | * INTERNAL: |
||
1601 | * Tries to get an entity by its identifier hash. If no entity is found for |
||
1602 | * the given hash, FALSE is returned. |
||
1603 | * |
||
1604 | * @ignore |
||
1605 | * |
||
1606 | * @param mixed $idHash (must be possible to cast it to string) |
||
1607 | * @param string $rootClassName |
||
1608 | * |
||
1609 | * @return object|bool The found entity or FALSE. |
||
1610 | */ |
||
1611 | 35 | View Code Duplication | public function tryGetByIdHash($idHash, $rootClassName) |
1619 | |||
1620 | /** |
||
1621 | * Checks whether an entity is registered in the identity map of this UnitOfWork. |
||
1622 | * |
||
1623 | * @param object $entity |
||
1624 | * |
||
1625 | * @return boolean |
||
1626 | */ |
||
1627 | 216 | public function isInIdentityMap($entity) |
|
1640 | |||
1641 | /** |
||
1642 | * INTERNAL: |
||
1643 | * Checks whether an identifier hash exists in the identity map. |
||
1644 | * |
||
1645 | * @ignore |
||
1646 | * |
||
1647 | * @param string $idHash |
||
1648 | * @param string $rootClassName |
||
1649 | * |
||
1650 | * @return boolean |
||
1651 | */ |
||
1652 | public function containsIdHash($idHash, $rootClassName) |
||
1656 | |||
1657 | /** |
||
1658 | * Persists an entity as part of the current unit of work. |
||
1659 | * |
||
1660 | * @param object $entity The entity to persist. |
||
1661 | * |
||
1662 | * @return void |
||
1663 | */ |
||
1664 | 1066 | public function persist($entity) |
|
1670 | |||
1671 | /** |
||
1672 | * Persists an entity as part of the current unit of work. |
||
1673 | * |
||
1674 | * This method is internally called during persist() cascades as it tracks |
||
1675 | * the already visited entities to prevent infinite recursions. |
||
1676 | * |
||
1677 | * @param object $entity The entity to persist. |
||
1678 | * @param array $visited The already visited entities. |
||
1679 | * |
||
1680 | * @return void |
||
1681 | * |
||
1682 | * @throws ORMInvalidArgumentException |
||
1683 | * @throws UnexpectedValueException |
||
1684 | */ |
||
1685 | 1066 | private function doPersist($entity, array &$visited) |
|
1733 | |||
1734 | /** |
||
1735 | * Deletes an entity as part of the current unit of work. |
||
1736 | * |
||
1737 | * @param object $entity The entity to remove. |
||
1738 | * |
||
1739 | * @return void |
||
1740 | */ |
||
1741 | 65 | public function remove($entity) |
|
1747 | |||
1748 | /** |
||
1749 | * Deletes an entity as part of the current unit of work. |
||
1750 | * |
||
1751 | * This method is internally called during delete() cascades as it tracks |
||
1752 | * the already visited entities to prevent infinite recursions. |
||
1753 | * |
||
1754 | * @param object $entity The entity to delete. |
||
1755 | * @param array $visited The map of the already visited entities. |
||
1756 | * |
||
1757 | * @return void |
||
1758 | * |
||
1759 | * @throws ORMInvalidArgumentException If the instance is a detached entity. |
||
1760 | * @throws UnexpectedValueException |
||
1761 | */ |
||
1762 | 65 | private function doRemove($entity, array &$visited) |
|
1802 | |||
1803 | /** |
||
1804 | * Merges the state of the given detached entity into this UnitOfWork. |
||
1805 | * |
||
1806 | * @param object $entity |
||
1807 | * |
||
1808 | * @return object The managed copy of the entity. |
||
1809 | * |
||
1810 | * @throws OptimisticLockException If the entity uses optimistic locking through a version |
||
1811 | * attribute and the version check against the managed copy fails. |
||
1812 | * |
||
1813 | * @todo Require active transaction!? OptimisticLockException may result in undefined state!? |
||
1814 | */ |
||
1815 | 43 | public function merge($entity) |
|
1821 | |||
1822 | /** |
||
1823 | * Executes a merge operation on an entity. |
||
1824 | * |
||
1825 | * @param object $entity |
||
1826 | * @param array $visited |
||
1827 | * @param object|null $prevManagedCopy |
||
1828 | * @param array|null $assoc |
||
1829 | * |
||
1830 | * @return object The managed copy of the entity. |
||
1831 | * |
||
1832 | * @throws OptimisticLockException If the entity uses optimistic locking through a version |
||
1833 | * attribute and the version check against the managed copy fails. |
||
1834 | * @throws ORMInvalidArgumentException If the entity instance is NEW. |
||
1835 | * @throws EntityNotFoundException if an assigned identifier is used in the entity, but none is provided |
||
1836 | */ |
||
1837 | 43 | private function doMerge($entity, array &$visited, $prevManagedCopy = null, array $assoc = []) |
|
1925 | |||
1926 | /** |
||
1927 | * @param ClassMetadata $class |
||
1928 | * @param object $entity |
||
1929 | * @param object $managedCopy |
||
1930 | * |
||
1931 | * @return void |
||
1932 | * |
||
1933 | * @throws OptimisticLockException |
||
1934 | */ |
||
1935 | 34 | private function ensureVersionMatch(ClassMetadata $class, $entity, $managedCopy) |
|
1952 | |||
1953 | /** |
||
1954 | * Tests if an entity is loaded - must either be a loaded proxy or not a proxy |
||
1955 | * |
||
1956 | * @param object $entity |
||
1957 | * |
||
1958 | * @return bool |
||
1959 | */ |
||
1960 | 41 | private function isLoaded($entity) |
|
1964 | |||
1965 | /** |
||
1966 | * Sets/adds associated managed copies into the previous entity's association field |
||
1967 | * |
||
1968 | * @param object $entity |
||
1969 | * @param array $association |
||
1970 | * @param object $previousManagedCopy |
||
1971 | * @param object $managedCopy |
||
1972 | * |
||
1973 | * @return void |
||
1974 | */ |
||
1975 | 6 | private function updateAssociationWithMergedEntity($entity, array $association, $previousManagedCopy, $managedCopy) |
|
1995 | |||
1996 | /** |
||
1997 | * Detaches an entity from the persistence management. It's persistence will |
||
1998 | * no longer be managed by Doctrine. |
||
1999 | * |
||
2000 | * @param object $entity The entity to detach. |
||
2001 | * |
||
2002 | * @return void |
||
2003 | */ |
||
2004 | 12 | public function detach($entity) |
|
2010 | |||
2011 | /** |
||
2012 | * Executes a detach operation on the given entity. |
||
2013 | * |
||
2014 | * @param object $entity |
||
2015 | * @param array $visited |
||
2016 | * @param boolean $noCascade if true, don't cascade detach operation. |
||
2017 | * |
||
2018 | * @return void |
||
2019 | */ |
||
2020 | 16 | private function doDetach($entity, array &$visited, $noCascade = false) |
|
2054 | |||
2055 | /** |
||
2056 | * Refreshes the state of the given entity from the database, overwriting |
||
2057 | * any local, unpersisted changes. |
||
2058 | * |
||
2059 | * @param object $entity The entity to refresh. |
||
2060 | * |
||
2061 | * @return void |
||
2062 | * |
||
2063 | * @throws InvalidArgumentException If the entity is not MANAGED. |
||
2064 | */ |
||
2065 | 17 | public function refresh($entity) |
|
2071 | |||
2072 | /** |
||
2073 | * Executes a refresh operation on an entity. |
||
2074 | * |
||
2075 | * @param object $entity The entity to refresh. |
||
2076 | * @param array $visited The already visited entities during cascades. |
||
2077 | * |
||
2078 | * @return void |
||
2079 | * |
||
2080 | * @throws ORMInvalidArgumentException If the entity is not MANAGED. |
||
2081 | */ |
||
2082 | 17 | private function doRefresh($entity, array &$visited) |
|
2105 | |||
2106 | /** |
||
2107 | * Cascades a refresh operation to associated entities. |
||
2108 | * |
||
2109 | * @param object $entity |
||
2110 | * @param array $visited |
||
2111 | * |
||
2112 | * @return void |
||
2113 | */ |
||
2114 | 17 | View Code Duplication | private function cascadeRefresh($entity, array &$visited) |
2148 | |||
2149 | /** |
||
2150 | * Cascades a detach operation to associated entities. |
||
2151 | * |
||
2152 | * @param object $entity |
||
2153 | * @param array $visited |
||
2154 | * |
||
2155 | * @return void |
||
2156 | */ |
||
2157 | 14 | View Code Duplication | private function cascadeDetach($entity, array &$visited) |
2191 | |||
2192 | /** |
||
2193 | * Cascades a merge operation to associated entities. |
||
2194 | * |
||
2195 | * @param object $entity |
||
2196 | * @param object $managedCopy |
||
2197 | * @param array $visited |
||
2198 | * |
||
2199 | * @return void |
||
2200 | */ |
||
2201 | 41 | private function cascadeMerge($entity, $managedCopy, array &$visited) |
|
2231 | |||
2232 | /** |
||
2233 | * Cascades the save operation to associated entities. |
||
2234 | * |
||
2235 | * @param object $entity |
||
2236 | * @param array $visited |
||
2237 | * |
||
2238 | * @return void |
||
2239 | */ |
||
2240 | 1066 | private function cascadePersist($entity, array &$visited) |
|
2291 | |||
2292 | /** |
||
2293 | * Cascades the delete operation to associated entities. |
||
2294 | * |
||
2295 | * @param object $entity |
||
2296 | * @param array $visited |
||
2297 | * |
||
2298 | * @return void |
||
2299 | */ |
||
2300 | 65 | private function cascadeRemove($entity, array &$visited) |
|
2340 | |||
2341 | /** |
||
2342 | * Acquire a lock on the given entity. |
||
2343 | * |
||
2344 | * @param object $entity |
||
2345 | * @param int $lockMode |
||
2346 | * @param int $lockVersion |
||
2347 | * |
||
2348 | * @return void |
||
2349 | * |
||
2350 | * @throws ORMInvalidArgumentException |
||
2351 | * @throws TransactionRequiredException |
||
2352 | * @throws OptimisticLockException |
||
2353 | */ |
||
2354 | 10 | public function lock($entity, $lockMode, $lockVersion = null) |
|
2407 | |||
2408 | /** |
||
2409 | * Gets the CommitOrderCalculator used by the UnitOfWork to order commits. |
||
2410 | * |
||
2411 | * @return \Doctrine\ORM\Internal\CommitOrderCalculator |
||
2412 | */ |
||
2413 | 1043 | public function getCommitOrderCalculator() |
|
2417 | |||
2418 | /** |
||
2419 | * Clears the UnitOfWork. |
||
2420 | * |
||
2421 | * @param string|null $entityName if given, only entities of this type will get detached. |
||
2422 | * |
||
2423 | * @return void |
||
2424 | * |
||
2425 | * @throws ORMInvalidArgumentException if an invalid entity name is given |
||
2426 | */ |
||
2427 | 1262 | public function clear($entityName = null) |
|
2455 | |||
2456 | /** |
||
2457 | * INTERNAL: |
||
2458 | * Schedules an orphaned entity for removal. The remove() operation will be |
||
2459 | * invoked on that entity at the beginning of the next commit of this |
||
2460 | * UnitOfWork. |
||
2461 | * |
||
2462 | * @ignore |
||
2463 | * |
||
2464 | * @param object $entity |
||
2465 | * |
||
2466 | * @return void |
||
2467 | */ |
||
2468 | 17 | public function scheduleOrphanRemoval($entity) |
|
2472 | |||
2473 | /** |
||
2474 | * INTERNAL: |
||
2475 | * Cancels a previously scheduled orphan removal. |
||
2476 | * |
||
2477 | * @ignore |
||
2478 | * |
||
2479 | * @param object $entity |
||
2480 | * |
||
2481 | * @return void |
||
2482 | */ |
||
2483 | 117 | public function cancelOrphanRemoval($entity) |
|
2487 | |||
2488 | /** |
||
2489 | * INTERNAL: |
||
2490 | * Schedules a complete collection for removal when this UnitOfWork commits. |
||
2491 | * |
||
2492 | * @param PersistentCollection $coll |
||
2493 | * |
||
2494 | * @return void |
||
2495 | */ |
||
2496 | 14 | public function scheduleCollectionDeletion(PersistentCollection $coll) |
|
2506 | |||
2507 | /** |
||
2508 | * @param PersistentCollection $coll |
||
2509 | * |
||
2510 | * @return bool |
||
2511 | */ |
||
2512 | public function isCollectionScheduledForDeletion(PersistentCollection $coll) |
||
2516 | |||
2517 | /** |
||
2518 | * @param ClassMetadata $class |
||
2519 | * |
||
2520 | * @return \Doctrine\Common\Persistence\ObjectManagerAware|object |
||
2521 | */ |
||
2522 | 699 | private function newInstance($class) |
|
2532 | |||
2533 | /** |
||
2534 | * INTERNAL: |
||
2535 | * Creates an entity. Used for reconstitution of persistent entities. |
||
2536 | * |
||
2537 | * Internal note: Highly performance-sensitive method. |
||
2538 | * |
||
2539 | * @ignore |
||
2540 | * |
||
2541 | * @param string $className The name of the entity class. |
||
2542 | * @param array $data The data for the entity. |
||
2543 | * @param array $hints Any hints to account for during reconstitution/lookup of the entity. |
||
2544 | * |
||
2545 | * @return object The managed entity instance. |
||
2546 | * |
||
2547 | * @todo Rename: getOrCreateEntity |
||
2548 | */ |
||
2549 | 841 | public function createEntity($className, array $data, &$hints = []) |
|
2833 | |||
2834 | /** |
||
2835 | * @return void |
||
2836 | */ |
||
2837 | 906 | public function triggerEagerLoads() |
|
2859 | |||
2860 | /** |
||
2861 | * Initializes (loads) an uninitialized persistent collection of an entity. |
||
2862 | * |
||
2863 | * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize. |
||
2864 | * |
||
2865 | * @return void |
||
2866 | * |
||
2867 | * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733. |
||
2868 | */ |
||
2869 | 147 | public function loadCollection(PersistentCollection $collection) |
|
2886 | |||
2887 | /** |
||
2888 | * Gets the identity map of the UnitOfWork. |
||
2889 | * |
||
2890 | * @return array |
||
2891 | */ |
||
2892 | 2 | public function getIdentityMap() |
|
2896 | |||
2897 | /** |
||
2898 | * Gets the original data of an entity. The original data is the data that was |
||
2899 | * present at the time the entity was reconstituted from the database. |
||
2900 | * |
||
2901 | * @param object $entity |
||
2902 | * |
||
2903 | * @return array |
||
2904 | */ |
||
2905 | 120 | public function getOriginalEntityData($entity) |
|
2913 | |||
2914 | /** |
||
2915 | * @ignore |
||
2916 | * |
||
2917 | * @param object $entity |
||
2918 | * @param array $data |
||
2919 | * |
||
2920 | * @return void |
||
2921 | */ |
||
2922 | public function setOriginalEntityData($entity, array $data) |
||
2926 | |||
2927 | /** |
||
2928 | * INTERNAL: |
||
2929 | * Sets a property value of the original data array of an entity. |
||
2930 | * |
||
2931 | * @ignore |
||
2932 | * |
||
2933 | * @param string $oid |
||
2934 | * @param string $property |
||
2935 | * @param mixed $value |
||
2936 | * |
||
2937 | * @return void |
||
2938 | */ |
||
2939 | 312 | public function setOriginalEntityProperty($oid, $property, $value) |
|
2943 | |||
2944 | /** |
||
2945 | * Gets the identifier of an entity. |
||
2946 | * The returned value is always an array of identifier values. If the entity |
||
2947 | * has a composite identifier then the identifier values are in the same |
||
2948 | * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames(). |
||
2949 | * |
||
2950 | * @param object $entity |
||
2951 | * |
||
2952 | * @return array The identifier values. |
||
2953 | */ |
||
2954 | 860 | public function getEntityIdentifier($entity) |
|
2958 | |||
2959 | /** |
||
2960 | * Processes an entity instance to extract their identifier values. |
||
2961 | * |
||
2962 | * @param object $entity The entity instance. |
||
2963 | * |
||
2964 | * @return mixed A scalar value. |
||
2965 | * |
||
2966 | * @throws \Doctrine\ORM\ORMInvalidArgumentException |
||
2967 | */ |
||
2968 | 127 | public function getSingleIdentifierValue($entity) |
|
2982 | |||
2983 | /** |
||
2984 | * Tries to find an entity with the given identifier in the identity map of |
||
2985 | * this UnitOfWork. |
||
2986 | * |
||
2987 | * @param mixed $id The entity identifier to look for. |
||
2988 | * @param string $rootClassName The name of the root class of the mapped entity hierarchy. |
||
2989 | * |
||
2990 | * @return object|bool Returns the entity with the specified identifier if it exists in |
||
2991 | * this UnitOfWork, FALSE otherwise. |
||
2992 | */ |
||
2993 | 545 | View Code Duplication | public function tryGetById($id, $rootClassName) |
3001 | |||
3002 | /** |
||
3003 | * Schedules an entity for dirty-checking at commit-time. |
||
3004 | * |
||
3005 | * @param object $entity The entity to schedule for dirty-checking. |
||
3006 | * |
||
3007 | * @return void |
||
3008 | * |
||
3009 | * @todo Rename: scheduleForSynchronization |
||
3010 | */ |
||
3011 | 5 | public function scheduleForDirtyCheck($entity) |
|
3017 | |||
3018 | /** |
||
3019 | * Checks whether the UnitOfWork has any pending insertions. |
||
3020 | * |
||
3021 | * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise. |
||
3022 | */ |
||
3023 | public function hasPendingInsertions() |
||
3027 | |||
3028 | /** |
||
3029 | * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the |
||
3030 | * number of entities in the identity map. |
||
3031 | * |
||
3032 | * @return integer |
||
3033 | */ |
||
3034 | 1 | public function size() |
|
3040 | |||
3041 | /** |
||
3042 | * Gets the EntityPersister for an Entity. |
||
3043 | * |
||
3044 | * @param string $entityName The name of the Entity. |
||
3045 | * |
||
3046 | * @return \Doctrine\ORM\Persisters\Entity\EntityPersister |
||
3047 | */ |
||
3048 | 1108 | public function getEntityPersister($entityName) |
|
3084 | |||
3085 | /** |
||
3086 | * Gets a collection persister for a collection-valued association. |
||
3087 | * |
||
3088 | * @param array $association |
||
3089 | * |
||
3090 | * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister |
||
3091 | */ |
||
3092 | 574 | public function getCollectionPersister(array $association) |
|
3117 | |||
3118 | /** |
||
3119 | * INTERNAL: |
||
3120 | * Registers an entity as managed. |
||
3121 | * |
||
3122 | * @param object $entity The entity. |
||
3123 | * @param array $id The identifier values. |
||
3124 | * @param array $data The original entity data. |
||
3125 | * |
||
3126 | * @return void |
||
3127 | */ |
||
3128 | 209 | public function registerManaged($entity, array $id, array $data) |
|
3142 | |||
3143 | /** |
||
3144 | * INTERNAL: |
||
3145 | * Clears the property changeset of the entity with the given OID. |
||
3146 | * |
||
3147 | * @param string $oid The entity's OID. |
||
3148 | * |
||
3149 | * @return void |
||
3150 | */ |
||
3151 | 15 | public function clearEntityChangeSet($oid) |
|
3155 | |||
3156 | /* PropertyChangedListener implementation */ |
||
3157 | |||
3158 | /** |
||
3159 | * Notifies this UnitOfWork of a property change in an entity. |
||
3160 | * |
||
3161 | * @param object $entity The entity that owns the property. |
||
3162 | * @param string $propertyName The name of the property that changed. |
||
3163 | * @param mixed $oldValue The old value of the property. |
||
3164 | * @param mixed $newValue The new value of the property. |
||
3165 | * |
||
3166 | * @return void |
||
3167 | */ |
||
3168 | 3 | public function propertyChanged($entity, $propertyName, $oldValue, $newValue) |
|
3186 | |||
3187 | /** |
||
3188 | * Gets the currently scheduled entity insertions in this UnitOfWork. |
||
3189 | * |
||
3190 | * @return array |
||
3191 | */ |
||
3192 | 2 | public function getScheduledEntityInsertions() |
|
3196 | |||
3197 | /** |
||
3198 | * Gets the currently scheduled entity updates in this UnitOfWork. |
||
3199 | * |
||
3200 | * @return array |
||
3201 | */ |
||
3202 | 3 | public function getScheduledEntityUpdates() |
|
3206 | |||
3207 | /** |
||
3208 | * Gets the currently scheduled entity deletions in this UnitOfWork. |
||
3209 | * |
||
3210 | * @return array |
||
3211 | */ |
||
3212 | 1 | public function getScheduledEntityDeletions() |
|
3216 | |||
3217 | /** |
||
3218 | * Gets the currently scheduled complete collection deletions |
||
3219 | * |
||
3220 | * @return array |
||
3221 | */ |
||
3222 | 1 | public function getScheduledCollectionDeletions() |
|
3226 | |||
3227 | /** |
||
3228 | * Gets the currently scheduled collection inserts, updates and deletes. |
||
3229 | * |
||
3230 | * @return array |
||
3231 | */ |
||
3232 | public function getScheduledCollectionUpdates() |
||
3236 | |||
3237 | /** |
||
3238 | * Helper method to initialize a lazy loading proxy or persistent collection. |
||
3239 | * |
||
3240 | * @param object $obj |
||
3241 | * |
||
3242 | * @return void |
||
3243 | */ |
||
3244 | 2 | public function initializeObject($obj) |
|
3256 | |||
3257 | /** |
||
3258 | * Helper method to show an object as string. |
||
3259 | * |
||
3260 | * @param object $obj |
||
3261 | * |
||
3262 | * @return string |
||
3263 | */ |
||
3264 | 1 | private static function objToStr($obj) |
|
3268 | |||
3269 | /** |
||
3270 | * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit(). |
||
3271 | * |
||
3272 | * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information |
||
3273 | * on this object that might be necessary to perform a correct update. |
||
3274 | * |
||
3275 | * @param object $object |
||
3276 | * |
||
3277 | * @return void |
||
3278 | * |
||
3279 | * @throws ORMInvalidArgumentException |
||
3280 | */ |
||
3281 | 6 | public function markReadOnly($object) |
|
3289 | |||
3290 | /** |
||
3291 | * Is this entity read only? |
||
3292 | * |
||
3293 | * @param object $object |
||
3294 | * |
||
3295 | * @return bool |
||
3296 | * |
||
3297 | * @throws ORMInvalidArgumentException |
||
3298 | */ |
||
3299 | 3 | public function isReadOnly($object) |
|
3307 | |||
3308 | /** |
||
3309 | * Perform whatever processing is encapsulated here after completion of the transaction. |
||
3310 | */ |
||
3311 | private function afterTransactionComplete() |
||
3317 | |||
3318 | /** |
||
3319 | * Perform whatever processing is encapsulated here after completion of the rolled-back. |
||
3320 | */ |
||
3321 | private function afterTransactionRolledBack() |
||
3327 | |||
3328 | /** |
||
3329 | * Performs an action after the transaction. |
||
3330 | * |
||
3331 | * @param callable $callback |
||
3332 | */ |
||
3333 | 1043 | private function performCallbackOnCachedPersister(callable $callback) |
|
3345 | |||
3346 | 1047 | private function dispatchOnFlushEvent() |
|
3352 | |||
3353 | 1042 | private function dispatchPostFlushEvent() |
|
3359 | |||
3360 | /** |
||
3361 | * Verifies if two given entities actually are the same based on identifier comparison |
||
3362 | * |
||
3363 | * @param object $entity1 |
||
3364 | * @param object $entity2 |
||
3365 | * |
||
3366 | * @return bool |
||
3367 | */ |
||
3368 | 14 | private function isIdentifierEquals($entity1, $entity2) |
|
3392 | |||
3393 | /** |
||
3394 | * @throws ORMInvalidArgumentException |
||
3395 | */ |
||
3396 | 1045 | private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void |
|
3408 | |||
3409 | /** |
||
3410 | * @param object $entity |
||
3411 | * @param object $managedCopy |
||
3412 | * |
||
3413 | * @throws ORMException |
||
3414 | * @throws OptimisticLockException |
||
3415 | * @throws TransactionRequiredException |
||
3416 | */ |
||
3417 | 40 | private function mergeEntityStateIntoManagedCopy($entity, $managedCopy) |
|
3516 | |||
3517 | /** |
||
3518 | * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle. |
||
3519 | * Unit of work able to fire deferred events, related to loading events here. |
||
3520 | * |
||
3521 | * @internal should be called internally from object hydrators |
||
3522 | */ |
||
3523 | 920 | public function hydrationComplete() |
|
3527 | |||
3528 | /** |
||
3529 | * @param string $entityName |
||
3530 | */ |
||
3531 | 4 | private function clearIdentityMapForEntityName($entityName) |
|
3543 | |||
3544 | /** |
||
3545 | * @param string $entityName |
||
3546 | */ |
||
3547 | 4 | private function clearEntityInsertionsForEntityName($entityName) |
|
3556 | |||
3557 | /** |
||
3558 | * @param ClassMetadata $class |
||
3559 | * @param mixed $identifierValue |
||
3560 | * |
||
3561 | * @return mixed the identifier after type conversion |
||
3562 | * |
||
3563 | * @throws \Doctrine\ORM\Mapping\MappingException if the entity has more than a single identifier |
||
3564 | */ |
||
3565 | 945 | private function convertSingleFieldIdentifierToPHPValue(ClassMetadata $class, $identifierValue) |
|
3572 | } |
||
3573 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.