Failed Conditions
Push — master ( ddb3cd...4476ec )
by Marco
11:47
created

UnitOfWork   F

Complexity

Total Complexity 406

Size/Duplication

Total Lines 2834
Duplicated Lines 0 %

Test Coverage

Coverage 89.8%

Importance

Changes 0
Metric Value
dl 0
loc 2834
ccs 933
cts 1039
cp 0.898
rs 0.6314
c 0
b 0
f 0
wmc 406

81 Methods

Rating   Name   Duplication   Size   Complexity  
B getCollectionPersister() 0 24 6
A remove() 0 5 1
B scheduleForUpdate() 0 14 5
A markReadOnly() 0 7 3
C recomputeSingleEntityChangeSet() 0 63 18
A getScheduledCollectionUpdates() 0 3 1
A isInIdentityMap() 0 12 2
A setOriginalEntityData() 0 3 1
A assertThatThereAreNoUnintentionallyNonPersistedAssociations() 0 9 2
A scheduleCollectionDeletion() 0 9 1
C cascadeRemove() 0 36 11
A containsIdHash() 0 3 1
A isScheduledForDelete() 0 3 1
A refresh() 0 5 1
A getOriginalEntityData() 0 5 1
A isEntityScheduled() 0 7 3
A addToIdentityMap() 0 19 4
B executeInserts() 0 32 5
A clear() 0 21 1
A getScheduledEntityDeletions() 0 3 1
A loadCollection() 0 12 2
A clearEntityChangeSet() 0 3 1
A cancelOrphanRemoval() 0 3 1
C executeDeletions() 0 35 7
A scheduleExtraUpdate() 0 12 2
A __construct() 0 9 1
A computeScheduleInsertsChangeSets() 0 6 2
C computeChangeSets() 0 40 11
A getScheduledEntityInsertions() 0 3 1
C getEntityState() 0 71 16
A getIdentityMap() 0 3 1
A getByIdHash() 0 3 1
C computeAssociationChanges() 0 64 14
A scheduleForSynchronization() 0 5 1
A getScheduledEntityUpdates() 0 3 1
A isScheduledForDirtyCheck() 0 5 1
F computeChangeSet() 0 176 48
A getEntityChangeSet() 0 10 2
A dispatchOnFlushEvent() 0 4 2
A tryGetById() 0 5 1
A afterTransactionComplete() 0 4 1
B scheduleForDelete() 0 25 5
A initializeObject() 0 10 3
A getSingleIdentifierValue() 0 14 3
F createEntity() 0 281 55
C scheduleForInsert() 0 27 8
A persist() 0 5 1
A isScheduledForInsert() 0 3 1
A setOriginalEntityProperty() 0 3 1
A hasPendingInsertions() 0 3 1
B isIdentifierEquals() 0 24 4
A objToStr() 0 3 2
D cascadeRefresh() 0 29 9
A isCollectionScheduledForDeletion() 0 3 1
A registerManaged() 0 13 4
C getCommitOrder() 0 67 11
A executeExtraUpdates() 0 14 2
A afterTransactionRolledBack() 0 4 1
A scheduleOrphanRemoval() 0 3 1
C doPersist() 0 49 7
A getEntityIdentifier() 0 3 1
A newInstance() 0 9 2
A size() 0 3 1
A propertyChanged() 0 15 3
A removeFromIdentityMap() 0 21 3
B executeUpdates() 0 29 6
A isScheduledForUpdate() 0 3 1
C doRemove() 0 38 7
A dispatchPostFlushEvent() 0 4 2
A tryGetByIdHash() 0 5 1
A triggerEagerLoads() 0 19 4
C getEntityPersister() 0 35 7
A getScheduledCollectionDeletions() 0 3 1
A hydrationComplete() 0 3 1
F commit() 0 106 22
A performCallbackOnCachedPersister() 0 9 4
A persistNew() 0 21 3
A doRefresh() 0 22 3
A isReadOnly() 0 7 2
C cascadePersist() 0 53 13
C lock() 0 50 13

How to fix   Complexity   

Complex Class

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.

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
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\Common\NotifyPropertyChanged;
9
use Doctrine\Common\PropertyChangedListener;
10
use Doctrine\DBAL\LockMode;
11
use Doctrine\Instantiator\Instantiator;
12
use Doctrine\ORM\Cache\Persister\CachedPersister;
13
use Doctrine\ORM\Event\LifecycleEventArgs;
14
use Doctrine\ORM\Event\ListenersInvoker;
15
use Doctrine\ORM\Event\OnFlushEventArgs;
16
use Doctrine\ORM\Event\PostFlushEventArgs;
17
use Doctrine\ORM\Event\PreFlushEventArgs;
18
use Doctrine\ORM\Event\PreUpdateEventArgs;
19
use Doctrine\ORM\Internal\HydrationCompleteHandler;
20
use Doctrine\ORM\Mapping\AssociationMetadata;
21
use Doctrine\ORM\Mapping\ChangeTrackingPolicy;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Mapping\FetchMode;
24
use Doctrine\ORM\Mapping\FieldMetadata;
25
use Doctrine\ORM\Mapping\GeneratorType;
26
use Doctrine\ORM\Mapping\InheritanceType;
27
use Doctrine\ORM\Mapping\JoinColumnMetadata;
28
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
29
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
30
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
31
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
32
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
33
use Doctrine\ORM\Mapping\VersionFieldMetadata;
34
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
35
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
36
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
37
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
38
use Doctrine\ORM\Persisters\Entity\EntityPersister;
39
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
40
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
41
use Doctrine\ORM\Utility\NormalizeIdentifier;
42
use InvalidArgumentException;
43
use ProxyManager\Proxy\GhostObjectInterface;
44
use UnexpectedValueException;
45
46
/**
47
 * The UnitOfWork is responsible for tracking changes to objects during an
48
 * "object-level" transaction and for writing out changes to the database
49
 * in the correct order.
50
 *
51
 * Internal note: This class contains highly performance-sensitive code.
52
 */
53
class UnitOfWork implements PropertyChangedListener
54
{
55
    /**
56
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
57
     */
58
    public const STATE_MANAGED = 1;
59
60
    /**
61
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
62
     * and is not (yet) managed by an EntityManager.
63
     */
64
    public const STATE_NEW = 2;
65
66
    /**
67
     * A detached entity is an instance with persistent state and identity that is not
68
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
69
     */
70
    public const STATE_DETACHED = 3;
71
72
    /**
73
     * A removed entity instance is an instance with a persistent identity,
74
     * associated with an EntityManager, whose persistent state will be deleted
75
     * on commit.
76
     */
77
    public const STATE_REMOVED = 4;
78
79
    /**
80
     * Hint used to collect all primary keys of associated entities during hydration
81
     * and execute it in a dedicated query afterwards
82
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
83
     */
84
    public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
85
86
    /**
87
     * The identity map that holds references to all managed entities that have
88
     * an identity. The entities are grouped by their class name.
89
     * Since all classes in a hierarchy must share the same identifier set,
90
     * we always take the root class name of the hierarchy.
91
     *
92
     * @var object[]
93
     */
94
    private $identityMap = [];
95
96
    /**
97
     * Map of all identifiers of managed entities.
98
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
99
     * Values are maps of entity identifiers, where its key is the column name and the value is the raw value.
100
     *
101
     * @var mixed[][]
102
     */
103
    private $entityIdentifiers = [];
104
105
    /**
106
     * Map of the original entity data of managed entities.
107
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
108
     * Values are maps of entity data, where its key is the field name and the value is the converted
109
     * (convertToPHPValue) value.
110
     * This structure is used for calculating changesets at commit time.
111
     *
112
     * Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
113
     *           A value will only really be copied if the value in the entity is modified by the user.
114
     *
115
     * @var mixed[][]
116
     */
117
    private $originalEntityData = [];
118
119
    /**
120
     * Map of entity changes. Keys are object ids (spl_object_id).
121
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
122
     *
123
     * @var mixed[][]
124
     */
125
    private $entityChangeSets = [];
126
127
    /**
128
     * The (cached) states of any known entities.
129
     * Keys are object ids (spl_object_id).
130
     *
131
     * @var int[]
132
     */
133
    private $entityStates = [];
134
135
    /**
136
     * Map of entities that are scheduled for dirty checking at commit time.
137
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
138
     * Keys are object ids (spl_object_id).
139
     *
140
     * @var object[]
141
     */
142
    private $scheduledForSynchronization = [];
143
144
    /**
145
     * A list of all pending entity insertions.
146
     *
147
     * @var object[]
148
     */
149
    private $entityInsertions = [];
150
151
    /**
152
     * A list of all pending entity updates.
153
     *
154
     * @var object[]
155
     */
156
    private $entityUpdates = [];
157
158
    /**
159
     * Any pending extra updates that have been scheduled by persisters.
160
     *
161
     * @var object[]
162
     */
163
    private $extraUpdates = [];
164
165
    /**
166
     * A list of all pending entity deletions.
167
     *
168
     * @var object[]
169
     */
170
    private $entityDeletions = [];
171
172
    /**
173
     * New entities that were discovered through relationships that were not
174
     * marked as cascade-persist. During flush, this array is populated and
175
     * then pruned of any entities that were discovered through a valid
176
     * cascade-persist path. (Leftovers cause an error.)
177
     *
178
     * Keys are OIDs, payload is a two-item array describing the association
179
     * and the entity.
180
     *
181
     * @var object[][]|array[][] indexed by respective object spl_object_id()
182
     */
183
    private $nonCascadedNewDetectedEntities = [];
184
185
    /**
186
     * All pending collection deletions.
187
     *
188
     * @var Collection[]|object[][]
189
     */
190
    private $collectionDeletions = [];
191
192
    /**
193
     * All pending collection updates.
194
     *
195
     * @var Collection[]|object[][]
196
     */
197
    private $collectionUpdates = [];
198
199
    /**
200
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
201
     * At the end of the UnitOfWork all these collections will make new snapshots
202
     * of their data.
203
     *
204
     * @var Collection[]|object[][]
205
     */
206
    private $visitedCollections = [];
207
208
    /**
209
     * The EntityManager that "owns" this UnitOfWork instance.
210
     *
211
     * @var EntityManagerInterface
212
     */
213
    private $em;
214
215
    /**
216
     * The entity persister instances used to persist entity instances.
217
     *
218
     * @var EntityPersister[]
219
     */
220
    private $entityPersisters = [];
221
222
    /**
223
     * The collection persister instances used to persist collections.
224
     *
225
     * @var CollectionPersister[]
226
     */
227
    private $collectionPersisters = [];
228
229
    /**
230
     * The EventManager used for dispatching events.
231
     *
232
     * @var \Doctrine\Common\EventManager
233
     */
234
    private $eventManager;
235
236
    /**
237
     * The ListenersInvoker used for dispatching events.
238
     *
239
     * @var \Doctrine\ORM\Event\ListenersInvoker
240
     */
241
    private $listenersInvoker;
242
243
    /**
244
     * @var Instantiator
245
     */
246
    private $instantiator;
247
248
    /**
249
     * Orphaned entities that are scheduled for removal.
250
     *
251
     * @var object[]
252
     */
253
    private $orphanRemovals = [];
254
255
    /**
256
     * Read-Only objects are never evaluated
257
     *
258
     * @var object[]
259
     */
260
    private $readOnlyObjects = [];
261
262
    /**
263
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
264
     *
265
     * @var mixed[][][]
266
     */
267
    private $eagerLoadingEntities = [];
268
269
    /**
270
     * @var bool
271
     */
272
    protected $hasCache = false;
273
274
    /**
275
     * Helper for handling completion of hydration
276
     *
277
     * @var HydrationCompleteHandler
278
     */
279
    private $hydrationCompleteHandler;
280
281
    /**
282
     * @var NormalizeIdentifier
283
     */
284
    private $normalizeIdentifier;
285
286
    /**
287
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
288
     */
289 2076
    public function __construct(EntityManagerInterface $em)
290
    {
291 2076
        $this->em                       = $em;
292 2076
        $this->eventManager             = $em->getEventManager();
293 2076
        $this->listenersInvoker         = new ListenersInvoker($em);
294 2076
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
295 2076
        $this->instantiator             = new Instantiator();
296 2076
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
297 2076
        $this->normalizeIdentifier      = new NormalizeIdentifier();
298 2076
    }
299
300
    /**
301
     * Commits the UnitOfWork, executing all operations that have been postponed
302
     * up to this point. The state of all managed entities will be synchronized with
303
     * the database.
304
     *
305
     * The operations are executed in the following order:
306
     *
307
     * 1) All entity insertions
308
     * 2) All entity updates
309
     * 3) All collection deletions
310
     * 4) All collection updates
311
     * 5) All entity deletions
312
     *
313
     * @throws \Exception
314
     */
315 942
    public function commit()
316
    {
317
        // Raise preFlush
318 942
        if ($this->eventManager->hasListeners(Events::preFlush)) {
319 2
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
320
        }
321
322 942
        $this->computeChangeSets();
323
324 940
        if (! ($this->entityInsertions ||
325 138
                $this->entityDeletions ||
326 106
                $this->entityUpdates ||
327 33
                $this->collectionUpdates ||
328 30
                $this->collectionDeletions ||
329 940
                $this->orphanRemovals)) {
330 18
            $this->dispatchOnFlushEvent();
331 18
            $this->dispatchPostFlushEvent();
332
333 18
            return; // Nothing to do.
334
        }
335
336 935
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
337
338 933
        if ($this->orphanRemovals) {
339 15
            foreach ($this->orphanRemovals as $orphan) {
340 15
                $this->remove($orphan);
341
            }
342
        }
343
344 933
        $this->dispatchOnFlushEvent();
345
346
        // Now we need a commit order to maintain referential integrity
347 933
        $commitOrder = $this->getCommitOrder();
348
349 933
        $conn = $this->em->getConnection();
350 933
        $conn->beginTransaction();
351
352
        try {
353
            // Collection deletions (deletions of complete collections)
354 933
            foreach ($this->collectionDeletions as $collectionToDelete) {
355 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
356
            }
357
358 933
            if ($this->entityInsertions) {
359 933
                foreach ($commitOrder as $class) {
360 933
                    $this->executeInserts($class);
361
                }
362
            }
363
364 932
            if ($this->entityUpdates) {
365 96
                foreach ($commitOrder as $class) {
366 96
                    $this->executeUpdates($class);
367
                }
368
            }
369
370
            // Extra updates that were requested by persisters.
371 932
            if ($this->extraUpdates) {
372 31
                $this->executeExtraUpdates();
373
            }
374
375
            // Collection updates (deleteRows, updateRows, insertRows)
376 932
            foreach ($this->collectionUpdates as $collectionToUpdate) {
377 512
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
378
            }
379
380
            // Entity deletions come last and need to be in reverse commit order
381 932
            if ($this->entityDeletions) {
382 55
                foreach (array_reverse($commitOrder) as $committedEntityName) {
383 55
                    if (! $this->entityDeletions) {
384 31
                        break; // just a performance optimisation
385
                    }
386
387 55
                    $this->executeDeletions($committedEntityName);
388
                }
389
            }
390
391 932
            $conn->commit();
392 6
        } catch (\Throwable $e) {
393 6
            $this->em->close();
394 6
            $conn->rollBack();
395
396 6
            $this->afterTransactionRolledBack();
397
398 6
            throw $e;
399
        }
400
401 932
        $this->afterTransactionComplete();
402
403
        // Take new snapshots from visited collections
404 932
        foreach ($this->visitedCollections as $coll) {
405 511
            $coll->takeSnapshot();
406
        }
407
408 932
        $this->dispatchPostFlushEvent();
409
410
        // Clean up
411 931
        $this->entityInsertions            =
412 931
        $this->entityUpdates               =
413 931
        $this->entityDeletions             =
414 931
        $this->extraUpdates                =
415 931
        $this->entityChangeSets            =
416 931
        $this->collectionUpdates           =
417 931
        $this->collectionDeletions         =
418 931
        $this->visitedCollections          =
419 931
        $this->scheduledForSynchronization =
420 931
        $this->orphanRemovals              = [];
421 931
    }
422
423
    /**
424
     * Computes the changesets of all entities scheduled for insertion.
425
     */
426 942
    private function computeScheduleInsertsChangeSets()
427
    {
428 942
        foreach ($this->entityInsertions as $entity) {
429 937
            $class = $this->em->getClassMetadata(get_class($entity));
430
431 937
            $this->computeChangeSet($class, $entity);
432
        }
433 940
    }
434
435
    /**
436
     * Executes any extra updates that have been scheduled.
437
     */
438 31
    private function executeExtraUpdates()
439
    {
440 31
        foreach ($this->extraUpdates as $oid => $update) {
441 31
            list ($entity, $changeset) = $update;
442
443 31
            $this->entityChangeSets[$oid] = $changeset;
444
445
//            echo 'Extra update: ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
446
//            \Doctrine\Common\Util\Debug::dump($changeset, 3);
447
448 31
            $this->getEntityPersister(get_class($entity))->update($entity);
449
        }
450
451 31
        $this->extraUpdates = [];
452 31
    }
453
454
    /**
455
     * Gets the changeset for an entity.
456
     *
457
     * @param object $entity
458
     *
459
     * @return mixed[]
460
     */
461 928
    public function & getEntityChangeSet($entity)
462
    {
463 928
        $oid  = spl_object_id($entity);
464 928
        $data = [];
465
466 928
        if (! isset($this->entityChangeSets[$oid])) {
467 2
            return $data;
468
        }
469
470 928
        return $this->entityChangeSets[$oid];
471
    }
472
473
    /**
474
     * Computes the changes that happened to a single entity.
475
     *
476
     * Modifies/populates the following properties:
477
     *
478
     * {@link originalEntityData}
479
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
480
     * then it was not fetched from the database and therefore we have no original
481
     * entity data yet. All of the current entity data is stored as the original entity data.
482
     *
483
     * {@link entityChangeSets}
484
     * The changes detected on all properties of the entity are stored there.
485
     * A change is a tuple array where the first entry is the old value and the second
486
     * entry is the new value of the property. Changesets are used by persisters
487
     * to INSERT/UPDATE the persistent entity state.
488
     *
489
     * {@link entityUpdates}
490
     * If the entity is already fully MANAGED (has been fetched from the database before)
491
     * and any changes to its properties are detected, then a reference to the entity is stored
492
     * there to mark it for an update.
493
     *
494
     * {@link collectionDeletions}
495
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
496
     * then this collection is marked for deletion.
497
     *
498
     * @ignore
499
     *
500
     * @internal Don't call from the outside.
501
     *
502
     * @param ClassMetadata $class  The class descriptor of the entity.
503
     * @param object        $entity The entity for which to compute the changes.
504
     *
505
     */
506 943
    public function computeChangeSet(ClassMetadata $class, $entity)
507
    {
508 943
        $oid = spl_object_id($entity);
509
510 943
        if (isset($this->readOnlyObjects[$oid])) {
511 1
            return;
512
        }
513
514 943
        if ($class->inheritanceType !== InheritanceType::NONE) {
515 323
            $class = $this->em->getClassMetadata(get_class($entity));
516
        }
517
518 943
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
519
520 943
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
521 117
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
522
        }
523
524 943
        $actualData = [];
525
526 943
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
527 943
            $value = $property->getValue($entity);
528
529 943
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
530 743
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
531 182
                    continue;
532
                }
533
534 740
                $value = $property->wrap($entity, $value, $this->em);
535
536 740
                $property->setValue($entity, $value);
537
538 740
                $actualData[$name] = $value;
539
540 740
                continue;
541
            }
542
543 943
            if (( ! $class->isIdentifier($name)
544 943
                    || ! $class->getProperty($name) instanceof FieldMetadata
545 943
                    || ! $class->getProperty($name)->hasValueGenerator()
546 943
                    || $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
547 943
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
548 943
                $actualData[$name] = $value;
549
            }
550
        }
551
552 943
        if (! isset($this->originalEntityData[$oid])) {
553
            // Entity is either NEW or MANAGED but not yet fully persisted (only has an id).
554
            // These result in an INSERT.
555 943
            $this->originalEntityData[$oid] = $actualData;
556 943
            $changeSet                      = [];
557
558 943
            foreach ($actualData as $propName => $actualValue) {
559 925
                $property = $class->getProperty($propName);
560
561 925
                if (($property instanceof FieldMetadata) ||
562 925
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
563 925
                    $changeSet[$propName] = [null, $actualValue];
564
                }
565
            }
566
567 943
            $this->entityChangeSets[$oid] = $changeSet;
568
        } else {
569
            // Entity is "fully" MANAGED: it was already fully persisted before
570
            // and we have a copy of the original data
571 235
            $originalData           = $this->originalEntityData[$oid];
572 235
            $isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY;
573 235
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
574
                ? $this->entityChangeSets[$oid]
575 235
                : [];
576
577 235
            foreach ($actualData as $propName => $actualValue) {
578
                // skip field, its a partially omitted one!
579 227
                if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
580 36
                    continue;
581
                }
582
583 227
                $orgValue = $originalData[$propName];
584
585
                // skip if value haven't changed
586 227
                if ($orgValue === $actualValue) {
587 216
                    continue;
588
                }
589
590 94
                $property = $class->getProperty($propName);
591
592
                // Persistent collection was exchanged with the "originally"
593
                // created one. This can only mean it was cloned and replaced
594
                // on another entity.
595 94
                if ($actualValue instanceof PersistentCollection) {
596 8
                    $owner = $actualValue->getOwner();
597
598 8
                    if ($owner === null) { // cloned
599
                        $actualValue->setOwner($entity, $property);
600 8
                    } elseif ($owner !== $entity) { // no clone, we have to fix
601
                        if (! $actualValue->isInitialized()) {
602
                            $actualValue->initialize(); // we have to do this otherwise the cols share state
603
                        }
604
605
                        $newValue = clone $actualValue;
606
607
                        $newValue->setOwner($entity, $property);
608
609
                        $property->setValue($entity, $newValue);
610
                    }
611
                }
612
613
                switch (true) {
614 94
                    case ($property instanceof FieldMetadata):
615 43
                        if ($isChangeTrackingNotify) {
616
                            // Continue inside switch behaves as break.
617
                            // We are required to use continue 2, since we need to continue to next $actualData item
618
                            continue 2;
619
                        }
620
621 43
                        $changeSet[$propName] = [$orgValue, $actualValue];
622 43
                        break;
623
624 55
                    case ($property instanceof ToOneAssociationMetadata):
625 46
                        if ($property->isOwningSide()) {
626 20
                            $changeSet[$propName] = [$orgValue, $actualValue];
627
                        }
628
629 46
                        if ($orgValue !== null && $property->isOrphanRemoval()) {
630 4
                            $this->scheduleOrphanRemoval($orgValue);
631
                        }
632
633 46
                        break;
634
635 10
                    case ($property instanceof ToManyAssociationMetadata):
636
                        // Check if original value exists
637 9
                        if ($orgValue instanceof PersistentCollection) {
638
                            // A PersistentCollection was de-referenced, so delete it.
639 8
                            if (! $this->isCollectionScheduledForDeletion($orgValue)) {
640 8
                                $this->scheduleCollectionDeletion($orgValue);
641
642 8
                                $changeSet[$propName] = $orgValue; // Signal changeset, to-many associations will be ignored
643
                            }
644
                        }
645
646 9
                        break;
647
648 94
                    default:
649
                        // Do nothing
650
                }
651
            }
652
653 235
            if ($changeSet) {
654 69
                $this->entityChangeSets[$oid]   = $changeSet;
655 69
                $this->originalEntityData[$oid] = $actualData;
656 69
                $this->entityUpdates[$oid]      = $entity;
657
            }
658
        }
659
660
        // Look for changes in associations of the entity
661 943
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
662 943
            if (! $property instanceof AssociationMetadata) {
663 943
                continue;
664
            }
665
666 835
            $value = $property->getValue($entity);
667
668 835
            if ($value === null) {
669 594
                continue;
670
            }
671
672 823
            $this->computeAssociationChanges($property, $value);
673
674 815
            if ($property instanceof ManyToManyAssociationMetadata &&
675 815
                $value instanceof PersistentCollection &&
676 815
                ! isset($this->entityChangeSets[$oid]) &&
677 815
                $property->isOwningSide() &&
678 815
                $value->isDirty()) {
679 30
                $this->entityChangeSets[$oid]   = [];
680 30
                $this->originalEntityData[$oid] = $actualData;
681 815
                $this->entityUpdates[$oid]      = $entity;
682
            }
683
        }
684 935
    }
685
686
    /**
687
     * Computes all the changes that have been done to entities and collections
688
     * since the last commit and stores these changes in the _entityChangeSet map
689
     * temporarily for access by the persisters, until the UoW commit is finished.
690
     */
691 942
    public function computeChangeSets()
692
    {
693
        // Compute changes for INSERTed entities first. This must always happen.
694 942
        $this->computeScheduleInsertsChangeSets();
695
696
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
697 940
        foreach ($this->identityMap as $className => $entities) {
698 421
            $class = $this->em->getClassMetadata($className);
699
700
            // Skip class if instances are read-only
701 421
            if ($class->isReadOnly()) {
702 1
                continue;
703
            }
704
705
            // If change tracking is explicit or happens through notification, then only compute
706
            // changes on entities of that type that are explicitly marked for synchronization.
707
            switch (true) {
708 420
                case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT):
709 418
                    $entitiesToProcess = $entities;
710 418
                    break;
711
712 3
                case (isset($this->scheduledForSynchronization[$className])):
713 3
                    $entitiesToProcess = $this->scheduledForSynchronization[$className];
714 3
                    break;
715
716
                default:
717 1
                    $entitiesToProcess = [];
718
            }
719
720 420
            foreach ($entitiesToProcess as $entity) {
721
                // Ignore uninitialized proxy objects
722 403
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
723 34
                    continue;
724
                }
725
726
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
727 402
                $oid = spl_object_id($entity);
728
729 402
                if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
730 420
                    $this->computeChangeSet($class, $entity);
731
                }
732
            }
733
        }
734 940
    }
735
736
    /**
737
     * Computes the changes of an association.
738
     *
739
     * @param AssociationMetadata $association The association mapping.
740
     * @param mixed               $value       The value of the association.
741
     *
742
     * @throws ORMInvalidArgumentException
743
     * @throws ORMException
744
     */
745 823
    private function computeAssociationChanges(AssociationMetadata $association, $value)
746
    {
747 823
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
748 28
            return;
749
        }
750
751 822
        if ($value instanceof PersistentCollection && $value->isDirty()) {
752 515
            $coid = spl_object_id($value);
753
754 515
            $this->collectionUpdates[$coid]  = $value;
755 515
            $this->visitedCollections[$coid] = $value;
756
        }
757
758
        // Look through the entities, and in any of their associations,
759
        // for transient (new) entities, recursively. ("Persistence by reachability")
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
760
        // Unwrap. Uninitialized collections will simply be empty.
761 822
        $unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap();
762 822
        $targetEntity   = $association->getTargetEntity();
763 822
        $targetClass    = $this->em->getClassMetadata($targetEntity);
764
765 822
        foreach ($unwrappedValue as $key => $entry) {
766 689
            if (! ($entry instanceof $targetEntity)) {
767 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
768
            }
769
770 681
            $state = $this->getEntityState($entry, self::STATE_NEW);
771
772 681
            if (! ($entry instanceof $targetEntity)) {
773
                throw ORMException::unexpectedAssociationValue(
774
                    $association->getSourceEntity(),
775
                    $association->getName(),
776
                    get_class($entry),
777
                    $targetEntity
778
                );
779
            }
780
781
            switch ($state) {
782 681
                case self::STATE_NEW:
783 36
                    if (! in_array('persist', $association->getCascade(), true)) {
784 5
                        $this->nonCascadedNewDetectedEntities[\spl_object_id($entry)] = [$association, $entry];
785
786 5
                        break;
787
                    }
788
789 32
                    $this->persistNew($targetClass, $entry);
790 32
                    $this->computeChangeSet($targetClass, $entry);
791
792 32
                    break;
793
794 675
                case self::STATE_REMOVED:
795
                    // Consume the $value as array (it's either an array or an ArrayAccess)
796
                    // and remove the element from Collection.
797 4
                    if ($association instanceof ToManyAssociationMetadata) {
798 3
                        unset($value[$key]);
799
                    }
800 4
                    break;
801
802 675
                case self::STATE_DETACHED:
803
                    // Can actually not happen right now as we assume STATE_NEW,
804
                    // so the exception will be raised from the DBAL layer (constraint violation).
805
                    throw ORMInvalidArgumentException::detachedEntityFoundThroughRelationship($association, $entry);
806
                    break;
807
808 681
                default:
809
                    // MANAGED associated entities are already taken into account
810
                    // during changeset calculation anyway, since they are in the identity map.
811
            }
812
        }
813 814
    }
814
815
    /**
816
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
817
     * @param object                              $entity
818
     */
819 958
    private function persistNew($class, $entity)
820
    {
821 958
        $oid    = spl_object_id($entity);
822 958
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
823
824 958
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
825 119
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
826
        }
827
828 958
        $generationPlan = $class->getValueGenerationPlan();
829 958
        $persister      = $this->getEntityPersister($class->getClassName());
830 958
        $generationPlan->executeImmediate($this->em, $entity);
831
832 958
        if (! $generationPlan->containsDeferred()) {
833 260
            $id                            = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
834 260
            $this->entityIdentifiers[$oid] = $id;
835
        }
836
837 958
        $this->entityStates[$oid] = self::STATE_MANAGED;
838
839 958
        $this->scheduleForInsert($entity);
840 958
    }
841
842
    /**
843
     * INTERNAL:
844
     * Computes the changeset of an individual entity, independently of the
845
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
846
     *
847
     * The passed entity must be a managed entity. If the entity already has a change set
848
     * because this method is invoked during a commit cycle then the change sets are added.
849
     * whereby changes detected in this method prevail.
850
     *
851
     * @ignore
852
     *
853
     * @param ClassMetadata $class  The class descriptor of the entity.
854
     * @param object        $entity The entity for which to (re)calculate the change set.
855
     *
856
     * @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
857
     * @throws \RuntimeException
858
     */
859 11
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void
860
    {
861 11
        $oid = spl_object_id($entity);
862
863 11
        if (! isset($this->entityStates[$oid]) || $this->entityStates[$oid] !== self::STATE_MANAGED) {
864
            throw ORMInvalidArgumentException::entityNotManaged($entity);
865
        }
866
867
        // skip if change tracking is "NOTIFY"
868 11
        if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
869
            return;
870
        }
871
872 11
        if ($class->inheritanceType !== InheritanceType::NONE) {
873 3
            $class = $this->em->getClassMetadata(get_class($entity));
874
        }
875
876 11
        $actualData = [];
877
878 11
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
879
            switch (true) {
880 11
                case ($property instanceof VersionFieldMetadata):
881
                    // Ignore version field
882
                    break;
883
884 11
                case ($property instanceof FieldMetadata):
885 11
                    if (! $property->isPrimaryKey()
886 11
                        || ! $property->getValueGenerator()
887 11
                        || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) {
888 11
                        $actualData[$name] = $property->getValue($entity);
889
                    }
890
891 11
                    break;
892
893 9
                case ($property instanceof ToOneAssociationMetadata):
894 8
                    $actualData[$name] = $property->getValue($entity);
895 11
                    break;
896
            }
897
        }
898
899 11
        if (! isset($this->originalEntityData[$oid])) {
900
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
901
        }
902
903 11
        $originalData = $this->originalEntityData[$oid];
904 11
        $changeSet    = [];
905
906 11
        foreach ($actualData as $propName => $actualValue) {
907 11
            $orgValue = $originalData[$propName] ?? null;
908
909 11
            if ($orgValue !== $actualValue) {
910 11
                $changeSet[$propName] = [$orgValue, $actualValue];
911
            }
912
        }
913
914 11
        if ($changeSet) {
915 4
            if (isset($this->entityChangeSets[$oid])) {
916 3
                $this->entityChangeSets[$oid] = array_merge($this->entityChangeSets[$oid], $changeSet);
917 1
            } elseif (! isset($this->entityInsertions[$oid])) {
918 1
                $this->entityChangeSets[$oid] = $changeSet;
919 1
                $this->entityUpdates[$oid]    = $entity;
920
            }
921 4
            $this->originalEntityData[$oid] = $actualData;
922
        }
923 11
    }
924
925
    /**
926
     * Executes all entity insertions for entities of the specified type.
927
     */
928 933
    private function executeInserts(ClassMetadata $class) : void
929
    {
930 933
        $className      = $class->getClassName();
931 933
        $persister      = $this->getEntityPersister($className);
932 933
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
933 933
        $generationPlan = $class->getValueGenerationPlan();
934
935 933
        foreach ($this->entityInsertions as $oid => $entity) {
936 933
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
937 809
                continue;
938
            }
939
940 933
            $persister->insert($entity);
941
942 932
            if ($generationPlan->containsDeferred()) {
943
                // Entity has post-insert IDs
944 841
                $oid = spl_object_id($entity);
945 841
                $id  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
946
947 841
                $this->entityIdentifiers[$oid]  = $id;
948 841
                $this->entityStates[$oid]       = self::STATE_MANAGED;
949 841
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
950
951 841
                $this->addToIdentityMap($entity);
952
            }
953
954 932
            unset($this->entityInsertions[$oid]);
955
956 932
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
957 115
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
958
959 932
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
960
            }
961
        }
962 933
    }
963
964
    /**
965
     * Executes all entity updates for entities of the specified type.
966
     *
967
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
968
     */
969 96
    private function executeUpdates($class)
970
    {
971 96
        $className        = $class->getClassName();
972 96
        $persister        = $this->getEntityPersister($className);
973 96
        $preUpdateInvoke  = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
974 96
        $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
975
976 96
        foreach ($this->entityUpdates as $oid => $entity) {
977 96
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
978 68
                continue;
979
            }
980
981 96
            if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
982 8
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
983
984 8
                $this->recomputeSingleEntityChangeSet($class, $entity);
985
            }
986
987 96
            if (! empty($this->entityChangeSets[$oid])) {
988
//                echo 'Update: ';
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
989
//                \Doctrine\Common\Util\Debug::dump($this->entityChangeSets[$oid], 3);
990
991 67
                $persister->update($entity);
992
            }
993
994 96
            unset($this->entityUpdates[$oid]);
995
996 96
            if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
997 96
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
998
            }
999
        }
1000 96
    }
1001
1002
    /**
1003
     * Executes all entity deletions for entities of the specified type.
1004
     *
1005
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1006
     */
1007 55
    private function executeDeletions($class)
1008
    {
1009 55
        $className = $class->getClassName();
1010 55
        $persister = $this->getEntityPersister($className);
1011 55
        $invoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1012
1013 55
        foreach ($this->entityDeletions as $oid => $entity) {
1014 55
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1015 23
                continue;
1016
            }
1017
1018 55
            $persister->delete($entity);
1019
1020
            unset(
1021 55
                $this->entityDeletions[$oid],
1022 55
                $this->entityIdentifiers[$oid],
1023 55
                $this->originalEntityData[$oid],
1024 55
                $this->entityStates[$oid]
1025
            );
1026
1027
            // Entity with this $oid after deletion treated as NEW, even if the $oid
1028
            // is obtained by a new entity because the old one went out of scope.
1029
            //$this->entityStates[$oid] = self::STATE_NEW;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1030 55
            if (! $class->isIdentifierComposite()) {
1031 52
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1032
1033 52
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1034 45
                    $property->setValue($entity, null);
1035
                }
1036
            }
1037
1038 55
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1039 8
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1040
1041 55
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1042
            }
1043
        }
1044 54
    }
1045
1046
    /**
1047
     * Gets the commit order.
1048
     *
1049
     * @return ClassMetadata[]
1050
     */
1051 933
    private function getCommitOrder()
1052
    {
1053 933
        $calc = new Internal\CommitOrderCalculator();
1054
1055
        // See if there are any new classes in the changeset, that are not in the
1056
        // commit order graph yet (don't have a node).
1057
        // We have to inspect changeSet to be able to correctly build dependencies.
1058
        // It is not possible to use IdentityMap here because post inserted ids
1059
        // are not yet available.
1060 933
        $newNodes = [];
1061
1062 933
        foreach (\array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1063 933
            $class = $this->em->getClassMetadata(get_class($entity));
1064
1065 933
            if ($calc->hasNode($class->getClassName())) {
1066 616
                continue;
1067
            }
1068
1069 933
            $calc->addNode($class->getClassName(), $class);
1070
1071 933
            $newNodes[] = $class;
1072
        }
1073
1074
        // Calculate dependencies for new nodes
1075 933
        while ($class = array_pop($newNodes)) {
1076 933
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1077 933
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1078 933
                    continue;
1079
                }
1080
1081 787
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1082
1083 787
                if (! $calc->hasNode($targetClass->getClassName())) {
1084 615
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1085
1086 615
                    $newNodes[] = $targetClass;
1087
                }
1088
1089 787
                $weight = ! array_filter(
1090 787
                    $property->getJoinColumns(),
1091 787
                    function (JoinColumnMetadata $joinColumn) {
1092 787
                        return $joinColumn->isNullable();
1093 787
                    }
1094
                );
1095
1096 787
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight);
1097
1098
                // If the target class has mapped subclasses, these share the same dependency.
1099 787
                if (! $targetClass->getSubClasses()) {
1100 783
                    continue;
1101
                }
1102
1103 231
                foreach ($targetClass->getSubClasses() as $subClassName) {
1104 231
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1105
1106 231
                    if (! $calc->hasNode($subClassName)) {
1107 204
                        $calc->addNode($targetSubClass->getClassName(), $targetSubClass);
1108
1109 204
                        $newNodes[] = $targetSubClass;
1110
                    }
1111
1112 231
                    $calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1);
1113
                }
1114
            }
1115
        }
1116
1117 933
        return $calc->sort();
1118
    }
1119
1120
    /**
1121
     * Schedules an entity for insertion into the database.
1122
     * If the entity already has an identifier, it will be added to the identity map.
1123
     *
1124
     * @param object $entity The entity to schedule for insertion.
1125
     *
1126
     * @throws ORMInvalidArgumentException
1127
     * @throws \InvalidArgumentException
1128
     */
1129 959
    public function scheduleForInsert($entity)
1130
    {
1131 959
        $oid = spl_object_id($entity);
1132
1133 959
        if (isset($this->entityUpdates[$oid])) {
1134
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1135
        }
1136
1137 959
        if (isset($this->entityDeletions[$oid])) {
1138 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1139
        }
1140 959
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1141 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1142
        }
1143
1144 959
        if (isset($this->entityInsertions[$oid])) {
1145 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1146
        }
1147
1148 959
        $this->entityInsertions[$oid] = $entity;
1149
1150 959
        if (isset($this->entityIdentifiers[$oid])) {
1151 260
            $this->addToIdentityMap($entity);
1152
        }
1153
1154 959
        if ($entity instanceof NotifyPropertyChanged) {
1155 5
            $entity->addPropertyChangedListener($this);
1156
        }
1157 959
    }
1158
1159
    /**
1160
     * Checks whether an entity is scheduled for insertion.
1161
     *
1162
     * @param object $entity
1163
     *
1164
     * @return bool
1165
     */
1166 593
    public function isScheduledForInsert($entity)
1167
    {
1168 593
        return isset($this->entityInsertions[spl_object_id($entity)]);
1169
    }
1170
1171
    /**
1172
     * Schedules an entity for being updated.
1173
     *
1174
     * @param object $entity The entity to schedule for being updated.
1175
     *
1176
     * @throws ORMInvalidArgumentException
1177
     */
1178 1
    public function scheduleForUpdate($entity) : void
1179
    {
1180 1
        $oid = spl_object_id($entity);
1181
1182 1
        if (! isset($this->entityIdentifiers[$oid])) {
1183
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'scheduling for update');
1184
        }
1185
1186 1
        if (isset($this->entityDeletions[$oid])) {
1187
            throw ORMInvalidArgumentException::entityIsRemoved($entity, 'schedule for update');
1188
        }
1189
1190 1
        if (! isset($this->entityUpdates[$oid]) && ! isset($this->entityInsertions[$oid])) {
1191 1
            $this->entityUpdates[$oid] = $entity;
1192
        }
1193 1
    }
1194
1195
    /**
1196
     * INTERNAL:
1197
     * Schedules an extra update that will be executed immediately after the
1198
     * regular entity updates within the currently running commit cycle.
1199
     *
1200
     * Extra updates for entities are stored as (entity, changeset) tuples.
1201
     *
1202
     * @ignore
1203
     *
1204
     * @param object  $entity    The entity for which to schedule an extra update.
1205
     * @param mixed[] $changeset The changeset of the entity (what to update).
1206
     *
1207
     */
1208 31
    public function scheduleExtraUpdate($entity, array $changeset) : void
1209
    {
1210 31
        $oid         = spl_object_id($entity);
1211 31
        $extraUpdate = [$entity, $changeset];
1212
1213 31
        if (isset($this->extraUpdates[$oid])) {
1214 1
            [$unused, $changeset2] = $this->extraUpdates[$oid];
1215
1216 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1217
        }
1218
1219 31
        $this->extraUpdates[$oid] = $extraUpdate;
1220 31
    }
1221
1222
    /**
1223
     * Checks whether an entity is registered as dirty in the unit of work.
1224
     * Note: Is not very useful currently as dirty entities are only registered
1225
     * at commit time.
1226
     *
1227
     * @param object $entity
1228
     */
1229
    public function isScheduledForUpdate($entity) : bool
1230
    {
1231
        return isset($this->entityUpdates[spl_object_id($entity)]);
1232
    }
1233
1234
    /**
1235
     * Checks whether an entity is registered to be checked in the unit of work.
1236
     *
1237
     * @param object $entity
1238
     */
1239 1
    public function isScheduledForDirtyCheck($entity) : bool
1240
    {
1241 1
        $rootEntityName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
1242
1243 1
        return isset($this->scheduledForSynchronization[$rootEntityName][spl_object_id($entity)]);
1244
    }
1245
1246
    /**
1247
     * INTERNAL:
1248
     * Schedules an entity for deletion.
1249
     *
1250
     * @param object $entity
1251
     */
1252 58
    public function scheduleForDelete($entity)
1253
    {
1254 58
        $oid = spl_object_id($entity);
1255
1256 58
        if (isset($this->entityInsertions[$oid])) {
1257 1
            if ($this->isInIdentityMap($entity)) {
1258
                $this->removeFromIdentityMap($entity);
1259
            }
1260
1261 1
            unset($this->entityInsertions[$oid], $this->entityStates[$oid]);
1262
1263 1
            return; // entity has not been persisted yet, so nothing more to do.
1264
        }
1265
1266 58
        if (! $this->isInIdentityMap($entity)) {
1267 1
            return;
1268
        }
1269
1270 57
        $this->removeFromIdentityMap($entity);
1271
1272 57
        unset($this->entityUpdates[$oid]);
1273
1274 57
        if (! isset($this->entityDeletions[$oid])) {
1275 57
            $this->entityDeletions[$oid] = $entity;
1276 57
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1277
        }
1278 57
    }
1279
1280
    /**
1281
     * Checks whether an entity is registered as removed/deleted with the unit
1282
     * of work.
1283
     *
1284
     * @param object $entity
1285
     *
1286
     * @return bool
1287
     */
1288 13
    public function isScheduledForDelete($entity)
1289
    {
1290 13
        return isset($this->entityDeletions[spl_object_id($entity)]);
1291
    }
1292
1293
    /**
1294
     * Checks whether an entity is scheduled for insertion, update or deletion.
1295
     *
1296
     * @param object $entity
1297
     *
1298
     * @return bool
1299
     */
1300
    public function isEntityScheduled($entity)
1301
    {
1302
        $oid = spl_object_id($entity);
1303
1304
        return isset($this->entityInsertions[$oid])
1305
            || isset($this->entityUpdates[$oid])
1306
            || isset($this->entityDeletions[$oid]);
1307
    }
1308
1309
    /**
1310
     * INTERNAL:
1311
     * Registers an entity in the identity map.
1312
     * Note that entities in a hierarchy are registered with the class name of
1313
     * the root entity.
1314
     *
1315
     * @ignore
1316
     *
1317
     * @param object $entity The entity to register.
1318
     *
1319
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1320
     *               the entity in question is already managed.
1321
     *
1322
     * @throws ORMInvalidArgumentException
1323
     */
1324 1025
    public function addToIdentityMap($entity)
1325
    {
1326 1025
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1327 1025
        $identifier    = $this->entityIdentifiers[spl_object_id($entity)];
1328
1329 1025
        if (empty($identifier) || in_array(null, $identifier, true)) {
1330 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
1331
        }
1332
1333 1019
        $idHash    = implode(' ', $identifier);
1334 1019
        $className = $classMetadata->getRootClassName();
1335
1336 1019
        if (isset($this->identityMap[$className][$idHash])) {
1337 30
            return false;
1338
        }
1339
1340 1019
        $this->identityMap[$className][$idHash] = $entity;
1341
1342 1019
        return true;
1343
    }
1344
1345
    /**
1346
     * Gets the state of an entity with regard to the current unit of work.
1347
     *
1348
     * @param object   $entity
1349
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1350
     *                         This parameter can be set to improve performance of entity state detection
1351
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1352
     *                         is either known or does not matter for the caller of the method.
1353
     *
1354
     * @return int The entity state.
1355
     */
1356 966
    public function getEntityState($entity, $assume = null)
1357
    {
1358 966
        $oid = spl_object_id($entity);
1359
1360 966
        if (isset($this->entityStates[$oid])) {
1361 709
            return $this->entityStates[$oid];
1362
        }
1363
1364 962
        if ($assume !== null) {
1365 959
            return $assume;
1366
        }
1367
1368
        // State can only be NEW or DETACHED, because MANAGED/REMOVED states are known.
1369
        // Note that you can not remember the NEW or DETACHED state in _entityStates since
1370
        // the UoW does not hold references to such objects and the object hash can be reused.
1371
        // More generally because the state may "change" between NEW/DETACHED without the UoW being aware of it.
1372 8
        $class     = $this->em->getClassMetadata(get_class($entity));
1373 8
        $persister = $this->getEntityPersister($class->getClassName());
1374 8
        $id        = $persister->getIdentifier($entity);
1375
1376 8
        if (! $id) {
1377 3
            return self::STATE_NEW;
1378
        }
1379
1380 6
        $flatId = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $id);
1381
1382 6
        if ($class->isIdentifierComposite()
1383 5
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
1384 6
            || ! $class->getProperty($class->getSingleIdentifierFieldName())->hasValueGenerator()
1385
        ) {
1386
            // Check for a version field, if available, to avoid a db lookup.
1387 5
            if ($class->isVersioned()) {
1388 1
                return $class->versionProperty->getValue($entity)
1389
                    ? self::STATE_DETACHED
1390 1
                    : self::STATE_NEW;
1391
            }
1392
1393
            // Last try before db lookup: check the identity map.
1394 4
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1395 1
                return self::STATE_DETACHED;
1396
            }
1397
1398
            // db lookup
1399 4
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1400
                return self::STATE_DETACHED;
1401
            }
1402
1403 4
            return self::STATE_NEW;
1404
        }
1405
1406 1
        if ($class->isIdentifierComposite()
1407 1
            || ! $class->getProperty($class->getSingleIdentifierFieldName()) instanceof FieldMetadata
1408 1
            || ! $class->getValueGenerationPlan()->containsDeferred()) {
1409
            // if we have a pre insert generator we can't be sure that having an id
1410
            // really means that the entity exists. We have to verify this through
1411
            // the last resort: a db lookup
1412
1413
            // Last try before db lookup: check the identity map.
1414
            if ($this->tryGetById($flatId, $class->getRootClassName())) {
1415
                return self::STATE_DETACHED;
1416
            }
1417
1418
            // db lookup
1419
            if ($this->getEntityPersister($class->getClassName())->exists($entity)) {
1420
                return self::STATE_DETACHED;
1421
            }
1422
1423
            return self::STATE_NEW;
1424
        }
1425
1426 1
        return self::STATE_DETACHED;
1427
    }
1428
1429
    /**
1430
     * INTERNAL:
1431
     * Removes an entity from the identity map. This effectively detaches the
1432
     * entity from the persistence management of Doctrine.
1433
     *
1434
     * @ignore
1435
     *
1436
     * @param object $entity
1437
     *
1438
     * @return bool
1439
     *
1440
     * @throws ORMInvalidArgumentException
1441
     */
1442 57
    public function removeFromIdentityMap($entity)
1443
    {
1444 57
        $oid           = spl_object_id($entity);
1445 57
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1446 57
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1447
1448 57
        if ($idHash === '') {
1449
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1450
        }
1451
1452 57
        $className = $classMetadata->getRootClassName();
1453
1454 57
        if (isset($this->identityMap[$className][$idHash])) {
1455 57
            unset($this->identityMap[$className][$idHash], $this->readOnlyObjects[$oid]);
1456
1457
            //$this->entityStates[$oid] = self::STATE_DETACHED;
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1458
1459 57
            return true;
1460
        }
1461
1462
        return false;
1463
    }
1464
1465
    /**
1466
     * INTERNAL:
1467
     * Gets an entity in the identity map by its identifier hash.
1468
     *
1469
     * @ignore
1470
     *
1471
     * @param string $idHash
1472
     * @param string $rootClassName
1473
     *
1474
     * @return object
1475
     */
1476 6
    public function getByIdHash($idHash, $rootClassName)
1477
    {
1478 6
        return $this->identityMap[$rootClassName][$idHash];
1479
    }
1480
1481
    /**
1482
     * INTERNAL:
1483
     * Tries to get an entity by its identifier hash. If no entity is found for
1484
     * the given hash, FALSE is returned.
1485
     *
1486
     * @ignore
1487
     *
1488
     * @param mixed  $idHash        (must be possible to cast it to string)
1489
     * @param string $rootClassName
1490
     *
1491
     * @return object|bool The found entity or FALSE.
1492
     */
1493
    public function tryGetByIdHash($idHash, $rootClassName)
1494
    {
1495
        $stringIdHash = (string) $idHash;
1496
1497
        return $this->identityMap[$rootClassName][$stringIdHash] ?? false;
1498
    }
1499
1500
    /**
1501
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
1502
     *
1503
     * @param object $entity
1504
     *
1505
     * @return bool
1506
     */
1507 133
    public function isInIdentityMap($entity)
1508
    {
1509 133
        $oid = spl_object_id($entity);
1510
1511 133
        if (empty($this->entityIdentifiers[$oid])) {
1512 21
            return false;
1513
        }
1514
1515 120
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1516 120
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1517
1518 120
        return isset($this->identityMap[$classMetadata->getRootClassName()][$idHash]);
1519
    }
1520
1521
    /**
1522
     * INTERNAL:
1523
     * Checks whether an identifier hash exists in the identity map.
1524
     *
1525
     * @ignore
1526
     *
1527
     * @param string $idHash
1528
     * @param string $rootClassName
1529
     *
1530
     * @return bool
1531
     */
1532
    public function containsIdHash($idHash, $rootClassName)
1533
    {
1534
        return isset($this->identityMap[$rootClassName][$idHash]);
1535
    }
1536
1537
    /**
1538
     * Persists an entity as part of the current unit of work.
1539
     *
1540
     * @param object $entity The entity to persist.
1541
     */
1542 959
    public function persist($entity)
1543
    {
1544 959
        $visited = [];
1545
1546 959
        $this->doPersist($entity, $visited);
1547 952
    }
1548
1549
    /**
1550
     * Persists an entity as part of the current unit of work.
1551
     *
1552
     * This method is internally called during persist() cascades as it tracks
1553
     * the already visited entities to prevent infinite recursions.
1554
     *
1555
     * @param object   $entity  The entity to persist.
1556
     * @param object[] $visited The already visited entities.
1557
     *
1558
     * @throws ORMInvalidArgumentException
1559
     * @throws UnexpectedValueException
1560
     */
1561 959
    private function doPersist($entity, array &$visited)
1562
    {
1563 959
        $oid = spl_object_id($entity);
1564
1565 959
        if (isset($visited[$oid])) {
1566 109
            return; // Prevent infinite recursion
1567
        }
1568
1569 959
        $visited[$oid] = $entity; // Mark visited
1570
1571 959
        $class = $this->em->getClassMetadata(get_class($entity));
1572
1573
        // We assume NEW, so DETACHED entities result in an exception on flush (constraint violation).
1574
        // If we would detect DETACHED here we would throw an exception anyway with the same
1575
        // consequences (not recoverable/programming error), so just assuming NEW here
1576
        // lets us avoid some database lookups for entities with natural identifiers.
1577 959
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1578
1579
        switch ($entityState) {
1580 959
            case self::STATE_MANAGED:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1581
                // Nothing to do, except if policy is "deferred explicit"
1582 215
                if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) {
1583 2
                    $this->scheduleForSynchronization($entity);
1584
                }
1585 215
                break;
1586
1587 959
            case self::STATE_NEW:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1588 958
                $this->persistNew($class, $entity);
1589 958
                break;
1590
1591 1
            case self::STATE_REMOVED:
1592
                // Entity becomes managed again
1593 1
                unset($this->entityDeletions[$oid]);
1594 1
                $this->addToIdentityMap($entity);
1595
1596 1
                $this->entityStates[$oid] = self::STATE_MANAGED;
1597 1
                break;
1598
1599
            case self::STATE_DETACHED:
1600
                // Can actually not happen right now since we assume STATE_NEW.
1601
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'persisted');
1602
1603
            default:
1604
                throw new UnexpectedValueException(
1605
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1606
                );
1607
        }
1608
1609 959
        $this->cascadePersist($entity, $visited);
1610 952
    }
1611
1612
    /**
1613
     * Deletes an entity as part of the current unit of work.
1614
     *
1615
     * @param object $entity The entity to remove.
1616
     */
1617 57
    public function remove($entity)
1618
    {
1619 57
        $visited = [];
1620
1621 57
        $this->doRemove($entity, $visited);
1622 57
    }
1623
1624
    /**
1625
     * Deletes an entity as part of the current unit of work.
1626
     *
1627
     * This method is internally called during delete() cascades as it tracks
1628
     * the already visited entities to prevent infinite recursions.
1629
     *
1630
     * @param object   $entity  The entity to delete.
1631
     * @param object[] $visited The map of the already visited entities.
1632
     *
1633
     * @throws ORMInvalidArgumentException If the instance is a detached entity.
1634
     * @throws UnexpectedValueException
1635
     */
1636 57
    private function doRemove($entity, array &$visited)
1637
    {
1638 57
        $oid = spl_object_id($entity);
1639
1640 57
        if (isset($visited[$oid])) {
1641 1
            return; // Prevent infinite recursion
1642
        }
1643
1644 57
        $visited[$oid] = $entity; // mark visited
1645
1646
        // Cascade first, because scheduleForDelete() removes the entity from the identity map, which
1647
        // can cause problems when a lazy proxy has to be initialized for the cascade operation.
1648 57
        $this->cascadeRemove($entity, $visited);
1649
1650 57
        $class       = $this->em->getClassMetadata(get_class($entity));
1651 57
        $entityState = $this->getEntityState($entity);
1652
1653
        switch ($entityState) {
1654 57
            case self::STATE_NEW:
1655 57
            case self::STATE_REMOVED:
1656
                // nothing to do
1657 2
                break;
1658
1659 57
            case self::STATE_MANAGED:
1660 57
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1661
1662 57
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1663 7
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1664
                }
1665
1666 57
                $this->scheduleForDelete($entity);
1667 57
                break;
1668
1669
            case self::STATE_DETACHED:
1670
                throw ORMInvalidArgumentException::detachedEntityCannot($entity, 'removed');
1671
            default:
1672
                throw new UnexpectedValueException(
1673
                    sprintf('Unexpected entity state: %d.%s', $entityState, self::objToStr($entity))
1674
                );
1675
        }
1676 57
    }
1677
1678
    /**
1679
     * Refreshes the state of the given entity from the database, overwriting
1680
     * any local, unpersisted changes.
1681
     *
1682
     * @param object $entity The entity to refresh.
1683
     *
1684
     * @throws InvalidArgumentException If the entity is not MANAGED.
1685
     */
1686 11
    public function refresh($entity)
1687
    {
1688 11
        $visited = [];
1689
1690 11
        $this->doRefresh($entity, $visited);
1691 11
    }
1692
1693
    /**
1694
     * Executes a refresh operation on an entity.
1695
     *
1696
     * @param object   $entity  The entity to refresh.
1697
     * @param object[] $visited The already visited entities during cascades.
1698
     *
1699
     * @throws ORMInvalidArgumentException If the entity is not MANAGED.
1700
     */
1701 11
    private function doRefresh($entity, array &$visited)
1702
    {
1703 11
        $oid = spl_object_id($entity);
1704
1705 11
        if (isset($visited[$oid])) {
1706
            return; // Prevent infinite recursion
1707
        }
1708
1709 11
        $visited[$oid] = $entity; // mark visited
1710
1711 11
        $class = $this->em->getClassMetadata(get_class($entity));
1712
1713 11
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1714
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1715
        }
1716
1717 11
        $this->getEntityPersister($class->getClassName())->refresh(
1718 11
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1719 11
            $entity
1720
        );
1721
1722 11
        $this->cascadeRefresh($entity, $visited);
1723 11
    }
1724
1725
    /**
1726
     * Cascades a refresh operation to associated entities.
1727
     *
1728
     * @param object   $entity
1729
     * @param object[] $visited
1730
     */
1731 11
    private function cascadeRefresh($entity, array &$visited)
1732
    {
1733 11
        $class = $this->em->getClassMetadata(get_class($entity));
1734
1735 11
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1736 11
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade(), true))) {
1737 11
                continue;
1738
            }
1739
1740 4
            $relatedEntities = $association->getValue($entity);
1741
1742
            switch (true) {
1743 4
                case ($relatedEntities instanceof PersistentCollection):
1744
                    // Unwrap so that foreach() does not initialize
1745 4
                    $relatedEntities = $relatedEntities->unwrap();
1746
                    // break; is commented intentionally!
1747
1748
                case ($relatedEntities instanceof Collection):
1749
                case (is_array($relatedEntities)):
1750 4
                    foreach ($relatedEntities as $relatedEntity) {
1751
                        $this->doRefresh($relatedEntity, $visited);
1752
                    }
1753 4
                    break;
1754
1755
                case ($relatedEntities !== null):
1756
                    $this->doRefresh($relatedEntities, $visited);
1757
                    break;
1758
1759 4
                default:
1760
                    // Do nothing
1761
            }
1762
        }
1763 11
    }
1764
1765
    /**
1766
     * Cascades the save operation to associated entities.
1767
     *
1768
     * @param object   $entity
1769
     * @param object[] $visited
1770
     *
1771
     * @throws ORMInvalidArgumentException
1772
     */
1773 959
    private function cascadePersist($entity, array &$visited)
1774
    {
1775 959
        $class = $this->em->getClassMetadata(get_class($entity));
1776
1777 959
        if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1778
            // nothing to do - proxy is not initialized, therefore we don't do anything with it
1779
            return;
1780
        }
1781
1782 959
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1783 959
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade(), true))) {
1784 959
                continue;
1785
            }
1786
1787
            /** @var AssociationMetadata $association */
1788 617
            $relatedEntities = $association->getValue($entity);
1789 617
            $targetEntity    = $association->getTargetEntity();
1790
1791
            switch (true) {
1792 617
                case ($relatedEntities instanceof PersistentCollection):
1793
                    // Unwrap so that foreach() does not initialize
1794 13
                    $relatedEntities = $relatedEntities->unwrap();
1795
                    // break; is commented intentionally!
1796
1797 617
                case ($relatedEntities instanceof Collection):
1798 562
                case (is_array($relatedEntities)):
1799 515
                    if (! ($association instanceof ToManyAssociationMetadata)) {
1800 3
                        throw ORMInvalidArgumentException::invalidAssociation(
1801 3
                            $this->em->getClassMetadata($targetEntity),
1802 3
                            $association,
1803 3
                            $relatedEntities
1804
                        );
1805
                    }
1806
1807 512
                    foreach ($relatedEntities as $relatedEntity) {
1808 277
                        $this->doPersist($relatedEntity, $visited);
1809
                    }
1810
1811 512
                    break;
1812
1813 552
                case ($relatedEntities !== null):
1814 231
                    if (! $relatedEntities instanceof $targetEntity) {
1815 4
                        throw ORMInvalidArgumentException::invalidAssociation(
1816 4
                            $this->em->getClassMetadata($targetEntity),
1817 4
                            $association,
1818 4
                            $relatedEntities
1819
                        );
1820
                    }
1821
1822 227
                    $this->doPersist($relatedEntities, $visited);
1823 227
                    break;
1824
1825 611
                default:
1826
                    // Do nothing
1827
            }
1828
        }
1829 952
    }
1830
1831
    /**
1832
     * Cascades the delete operation to associated entities.
1833
     *
1834
     * @param object   $entity
1835
     * @param object[] $visited
1836
     */
1837 57
    private function cascadeRemove($entity, array &$visited)
1838
    {
1839 57
        $entitiesToCascade = [];
1840 57
        $class             = $this->em->getClassMetadata(get_class($entity));
1841
1842 57
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1843 57
            if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade(), true))) {
1844 57
                continue;
1845
            }
1846
1847 21
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1848 4
                $entity->initializeProxy();
1849
            }
1850
1851 21
            $relatedEntities = $association->getValue($entity);
1852
1853
            switch (true) {
1854 21
                case ($relatedEntities instanceof Collection):
1855 14
                case (\is_array($relatedEntities)):
1856
                    // If its a PersistentCollection initialization is intended! No unwrap!
1857 18
                    foreach ($relatedEntities as $relatedEntity) {
1858 10
                        $entitiesToCascade[] = $relatedEntity;
1859
                    }
1860 18
                    break;
1861
1862 14
                case ($relatedEntities !== null):
1863 6
                    $entitiesToCascade[] = $relatedEntities;
1864 6
                    break;
1865
1866 21
                default:
1867
                    // Do nothing
1868
            }
1869
        }
1870
1871 57
        foreach ($entitiesToCascade as $relatedEntity) {
1872 15
            $this->doRemove($relatedEntity, $visited);
1873
        }
1874 57
    }
1875
1876
    /**
1877
     * Acquire a lock on the given entity.
1878
     *
1879
     * @param object $entity
1880
     * @param int    $lockMode
1881
     * @param int    $lockVersion
1882
     *
1883
     * @throws ORMInvalidArgumentException
1884
     * @throws TransactionRequiredException
1885
     * @throws OptimisticLockException
1886
     * @throws \InvalidArgumentException
1887
     */
1888 3
    public function lock($entity, $lockMode, $lockVersion = null)
1889
    {
1890 3
        if ($entity === null) {
1891 1
            throw new \InvalidArgumentException('No entity passed to UnitOfWork#lock().');
1892
        }
1893
1894 2
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1895 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1896
        }
1897
1898 1
        $class = $this->em->getClassMetadata(get_class($entity));
1899
1900
        switch (true) {
1901 1
            case $lockMode === LockMode::OPTIMISTIC:
1902 1
                if (! $class->isVersioned()) {
1903
                    throw OptimisticLockException::notVersioned($class->getClassName());
1904
                }
1905
1906 1
                if ($lockVersion === null) {
1907
                    return;
1908
                }
1909
1910 1
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1911
                    $entity->initializeProxy();
1912
                }
1913
1914 1
                $entityVersion = $class->versionProperty->getValue($entity);
1915
1916 1
                if ($entityVersion !== $lockVersion) {
1917
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
1918
                }
1919
1920 1
                break;
1921
1922
            case $lockMode === LockMode::NONE:
1923
            case $lockMode === LockMode::PESSIMISTIC_READ:
1924
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1925
                if (! $this->em->getConnection()->isTransactionActive()) {
1926
                    throw TransactionRequiredException::transactionRequired();
1927
                }
1928
1929
                $oid = spl_object_id($entity);
1930
1931
                $this->getEntityPersister($class->getClassName())->lock(
1932
                    array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1933
                    $lockMode
1934
                );
1935
                break;
1936
1937
            default:
1938
                // Do nothing
1939
        }
1940 1
    }
1941
1942
    /**
1943
     * Clears the UnitOfWork.
1944
     */
1945 1092
    public function clear()
1946
    {
1947 1092
        $this->entityPersisters               =
1948 1092
        $this->collectionPersisters           =
1949 1092
        $this->eagerLoadingEntities           =
1950 1092
        $this->identityMap                    =
1951 1092
        $this->entityIdentifiers              =
1952 1092
        $this->originalEntityData             =
1953 1092
        $this->entityChangeSets               =
1954 1092
        $this->entityStates                   =
1955 1092
        $this->scheduledForSynchronization    =
1956 1092
        $this->entityInsertions               =
1957 1092
        $this->entityUpdates                  =
1958 1092
        $this->entityDeletions                =
1959 1092
        $this->collectionDeletions            =
1960 1092
        $this->collectionUpdates              =
1961 1092
        $this->extraUpdates                   =
1962 1092
        $this->readOnlyObjects                =
1963 1092
        $this->visitedCollections             =
1964 1092
        $this->nonCascadedNewDetectedEntities =
1965 1092
        $this->orphanRemovals                 = [];
1966 1092
    }
1967
1968
    /**
1969
     * INTERNAL:
1970
     * Schedules an orphaned entity for removal. The remove() operation will be
1971
     * invoked on that entity at the beginning of the next commit of this
1972
     * UnitOfWork.
1973
     *
1974
     * @ignore
1975
     *
1976
     * @param object $entity
1977
     */
1978 16
    public function scheduleOrphanRemoval($entity)
1979
    {
1980 16
        $this->orphanRemovals[spl_object_id($entity)] = $entity;
1981 16
    }
1982
1983
    /**
1984
     * INTERNAL:
1985
     * Cancels a previously scheduled orphan removal.
1986
     *
1987
     * @ignore
1988
     *
1989
     * @param object $entity
1990
     */
1991 110
    public function cancelOrphanRemoval($entity)
1992
    {
1993 110
        unset($this->orphanRemovals[spl_object_id($entity)]);
1994 110
    }
1995
1996
    /**
1997
     * INTERNAL:
1998
     * Schedules a complete collection for removal when this UnitOfWork commits.
1999
     */
2000 22
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2001
    {
2002 22
        $coid = spl_object_id($coll);
2003
2004
        // TODO: if $coll is already scheduled for recreation ... what to do?
2005
        // Just remove $coll from the scheduled recreations?
2006 22
        unset($this->collectionUpdates[$coid]);
2007
2008 22
        $this->collectionDeletions[$coid] = $coll;
2009 22
    }
2010
2011
    /**
2012
     * @return bool
2013
     */
2014 8
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2015
    {
2016 8
        return isset($this->collectionDeletions[spl_object_id($coll)]);
2017
    }
2018
2019
    /**
2020
     * INTERNAL:
2021
     * Creates a new instance of the mapped class, without invoking the constructor.
2022
     * This is only meant to be used internally, and should not be consumed by end users.
2023
     *
2024
     * @ignore
2025
     *
2026
     * @return EntityManagerAware|object
2027
     */
2028 619
    public function newInstance(ClassMetadata $class)
2029
    {
2030 619
        $entity = $this->instantiator->instantiate($class->getClassName());
2031
2032 619
        if ($entity instanceof EntityManagerAware) {
2033 1
            $entity->injectEntityManager($this->em, $class);
2034
        }
2035
2036 619
        return $entity;
2037
    }
2038
2039
    /**
2040
     * INTERNAL:
2041
     * Creates an entity. Used for reconstitution of persistent entities.
2042
     *
2043
     * Internal note: Highly performance-sensitive method.
2044
     *
2045
     * @ignore
2046
     *
2047
     * @param string  $className The name of the entity class.
2048
     * @param mixed[] $data      The data for the entity.
2049
     * @param mixed[] $hints     Any hints to account for during reconstitution/lookup of the entity.
2050
     *
2051
     * @return object The managed entity instance.
2052
     *
2053
     * @todo Rename: getOrCreateEntity
2054
     */
2055 747
    public function createEntity($className, array $data, &$hints = [])
2056
    {
2057 747
        $class  = $this->em->getClassMetadata($className);
2058 747
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
2059 747
        $idHash = implode(' ', $id);
2060
2061 747
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
2062 284
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
2063 284
            $oid    = spl_object_id($entity);
2064
2065 284
            if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) {
2066 58
                $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
2067 58
                if ($unmanagedProxy !== $entity
2068 58
                    && $unmanagedProxy instanceof GhostObjectInterface
2069 58
                    && $this->isIdentifierEquals($unmanagedProxy, $entity)
2070
                ) {
2071
                    // We will hydrate the given un-managed proxy anyway:
2072
                    // continue work, but consider it the entity from now on
2073 3
                    $entity = $unmanagedProxy;
2074
                }
2075
            }
2076
2077 284
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
2078 16
                $entity->setProxyInitializer(null);
2079
2080 16
                if ($entity instanceof NotifyPropertyChanged) {
2081 16
                    $entity->addPropertyChangedListener($this);
2082
                }
2083
            } else {
2084 271
                if (! isset($hints[Query::HINT_REFRESH])
2085 271
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2086 220
                    return $entity;
2087
                }
2088
            }
2089
2090
            // inject EntityManager upon refresh.
2091 87
            if ($entity instanceof EntityManagerAware) {
2092 1
                $entity->injectEntityManager($this->em, $class);
2093
            }
2094
2095 87
            $this->originalEntityData[$oid] = $data;
2096
        } else {
2097 616
            $entity = $this->newInstance($class);
2098 616
            $oid    = spl_object_id($entity);
2099
2100 616
            $this->entityIdentifiers[$oid]  = $id;
2101 616
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2102 616
            $this->originalEntityData[$oid] = $data;
2103
2104 616
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
2105
        }
2106
2107 640
        if ($entity instanceof NotifyPropertyChanged) {
2108 3
            $entity->addPropertyChangedListener($this);
2109
        }
2110
2111 640
        foreach ($data as $field => $value) {
2112 640
            $property = $class->getProperty($field);
2113
2114 640
            if ($property instanceof FieldMetadata) {
2115 640
                $property->setValue($entity, $value);
2116
            }
2117
        }
2118
2119
        // Loading the entity right here, if its in the eager loading map get rid of it there.
2120 640
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2121
2122 640
        if (isset($this->eagerLoadingEntities[$class->getRootClassName()]) && ! $this->eagerLoadingEntities[$class->getRootClassName()]) {
2123
            unset($this->eagerLoadingEntities[$class->getRootClassName()]);
2124
        }
2125
2126
        // Properly initialize any unfetched associations, if partial objects are not allowed.
2127 640
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2128 34
            return $entity;
2129
        }
2130
2131 606
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
2132 606
            if (! ($association instanceof AssociationMetadata)) {
2133 606
                continue;
2134
            }
2135
2136
            // Check if the association is not among the fetch-joined associations already.
2137 527
            if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2138 222
                continue;
2139
            }
2140
2141 511
            $targetEntity = $association->getTargetEntity();
2142 511
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2143
2144 511
            if ($association instanceof ToManyAssociationMetadata) {
2145
                // Ignore if its a cached collection
2146 444
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2147 444
                    $association->getValue($entity) instanceof PersistentCollection) {
2148
                    continue;
2149
                }
2150
2151 444
                $hasDataField = isset($data[$field]);
2152
2153
                // use the given collection
2154 444
                if ($hasDataField && $data[$field] instanceof PersistentCollection) {
2155
                    $data[$field]->setOwner($entity, $association);
2156
2157
                    $association->setValue($entity, $data[$field]);
2158
2159
                    $this->originalEntityData[$oid][$field] = $data[$field];
2160
2161
                    continue;
2162
                }
2163
2164
                // Inject collection
2165 444
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2166
2167 444
                $pColl->setInitialized($hasDataField);
2168
2169 444
                $association->setValue($entity, $pColl);
2170
2171 444
                if ($association->getFetchMode() === FetchMode::EAGER) {
2172 4
                    $this->loadCollection($pColl);
2173 4
                    $pColl->takeSnapshot();
2174
                }
2175
2176 444
                $this->originalEntityData[$oid][$field] = $pColl;
2177
2178 444
                continue;
2179
            }
2180
2181 449
            if (! $association->isOwningSide()) {
2182
                // use the given entity association
2183 60
                if (isset($data[$field]) && is_object($data[$field]) &&
2184 60
                    isset($this->entityStates[spl_object_id($data[$field])])) {
2185 3
                    $inverseAssociation = $targetClass->getProperty($association->getMappedBy());
2186
2187 3
                    $association->setValue($entity, $data[$field]);
2188 3
                    $inverseAssociation->setValue($data[$field], $entity);
2189
2190 3
                    $this->originalEntityData[$oid][$field] = $data[$field];
2191
2192 3
                    continue;
2193
                }
2194
2195
                // Inverse side of x-to-one can never be lazy
2196 57
                $persister = $this->getEntityPersister($targetEntity);
2197
2198 57
                $association->setValue($entity, $persister->loadToOneEntity($association, $entity));
2199
2200 57
                continue;
2201
            }
2202
2203
            // use the entity association
2204 449
            if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2205 37
                $association->setValue($entity, $data[$field]);
2206
2207 37
                $this->originalEntityData[$oid][$field] = $data[$field];
2208
2209 37
                continue;
2210
            }
2211
2212 442
            $associatedId = [];
2213
2214
            // TODO: Is this even computed right in all cases of composite keys?
2215 442
            foreach ($association->getJoinColumns() as $joinColumn) {
2216
                /** @var JoinColumnMetadata $joinColumn */
2217 442
                $joinColumnName  = $joinColumn->getColumnName();
2218 442
                $joinColumnValue = $data[$joinColumnName] ?? null;
2219 442
                $targetField     = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
2220
2221 442
                if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) {
2222
                    // the missing key is part of target's entity primary key
2223 256
                    $associatedId = [];
2224
2225 256
                    continue;
2226
                }
2227
2228 272
                $associatedId[$targetField] = $joinColumnValue;
2229
            }
2230
2231 442
            if (! $associatedId) {
2232
                // Foreign key is NULL
2233 256
                $association->setValue($entity, null);
2234 256
                $this->originalEntityData[$oid][$field] = null;
2235
2236 256
                continue;
2237
            }
2238
2239
            // @todo guilhermeblanco Can we remove the need of this somehow?
2240 272
            if (! isset($hints['fetchMode'][$class->getClassName()][$field])) {
2241 269
                $hints['fetchMode'][$class->getClassName()][$field] = $association->getFetchMode();
2242
            }
2243
2244
            // Foreign key is set
2245
            // Check identity map first
2246
            // FIXME: Can break easily with composite keys if join column values are in
2247
            //        wrong order. The correct order is the one in ClassMetadata#identifier.
2248 272
            $relatedIdHash = implode(' ', $associatedId);
2249
2250
            switch (true) {
2251 272
                case (isset($this->identityMap[$targetClass->getRootClassName()][$relatedIdHash])):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2252 163
                    $newValue = $this->identityMap[$targetClass->getRootClassName()][$relatedIdHash];
2253
2254
                    // If this is an uninitialized proxy, we are deferring eager loads,
2255
                    // this association is marked as eager fetch, and its an uninitialized proxy (wtf!)
2256
                    // then we can append this entity for eager loading!
2257 163
                    if (! $targetClass->isIdentifierComposite() &&
2258 163
                        $newValue instanceof GhostObjectInterface &&
2259 163
                        isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2260 163
                        $hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER &&
2261 163
                        ! $newValue->isProxyInitialized()
2262
                    ) {
2263
                        $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
2264
                    }
2265
2266 163
                    break;
2267
2268 182
                case ($targetClass->getSubClasses()):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2269
                    // If it might be a subtype, it can not be lazy. There isn't even
2270
                    // a way to solve this with deferred eager loading, which means putting
2271
                    // an entity with subclasses at a *-to-one location is really bad! (performance-wise)
2272 29
                    $persister = $this->getEntityPersister($targetEntity);
2273 29
                    $newValue  = $persister->loadToOneEntity($association, $entity, $associatedId);
2274 29
                    break;
2275
2276
                default:
2277
                    // Proxies do not carry any kind of original entity data until they're fully loaded/initialized
2278 155
                    $managedData = [];
2279
2280 155
                    $normalizedAssociatedId = $this->normalizeIdentifier->__invoke(
2281 155
                        $this->em,
2282 155
                        $targetClass,
2283 155
                        $associatedId
2284
                    );
2285
2286
                    switch (true) {
2287
                        // We are negating the condition here. Other cases will assume it is valid!
2288 155
                        case ($hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER):
2289 152
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2290 152
                            break;
2291
2292
                        // Deferred eager load only works for single identifier classes
2293 3
                        case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite()):
2294
                            // TODO: Is there a faster approach?
2295 3
                            $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($normalizedAssociatedId);
2296
2297 3
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2298 3
                            break;
2299
2300
                        default:
2301
                            // TODO: This is very imperformant, ignore it?
2302
                            $newValue = $this->em->find($targetEntity, $normalizedAssociatedId);
2303
                            // Needed to re-assign original entity data for freshly loaded entity
2304
                            $managedData = $this->originalEntityData[spl_object_id($newValue)];
2305
                            break;
2306
                    }
2307
2308
                    // @TODO using `$associatedId` here seems to be risky.
2309 155
                    $this->registerManaged($newValue, $associatedId, $managedData);
2310
2311 155
                    break;
2312
            }
2313
2314 272
            $this->originalEntityData[$oid][$field] = $newValue;
2315 272
            $association->setValue($entity, $newValue);
2316
2317 272
            if ($association->getInversedBy()
2318 272
                && $association instanceof OneToOneAssociationMetadata
2319
                // @TODO refactor this
2320
                // we don't want to set any values in un-initialized proxies
2321
                && ! (
2322 52
                    $newValue instanceof GhostObjectInterface
2323 272
                    && ! $newValue->isProxyInitialized()
2324
                )
2325
            ) {
2326 19
                $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
2327
2328 272
                $inverseAssociation->setValue($newValue, $entity);
2329
            }
2330
        }
2331
2332
        // defer invoking of postLoad event to hydration complete step
2333 606
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2334
2335 606
        return $entity;
2336
    }
2337
2338 807
    public function triggerEagerLoads()
2339
    {
2340 807
        if (! $this->eagerLoadingEntities) {
2341 807
            return;
2342
        }
2343
2344
        // avoid infinite recursion
2345 3
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2346 3
        $this->eagerLoadingEntities = [];
2347
2348 3
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2349 3
            if (! $ids) {
2350
                continue;
2351
            }
2352
2353 3
            $class = $this->em->getClassMetadata($entityName);
2354
2355 3
            $this->getEntityPersister($entityName)->loadAll(
2356 3
                array_combine($class->identifier, [array_values($ids)])
2357
            );
2358
        }
2359 3
    }
2360
2361
    /**
2362
     * Initializes (loads) an uninitialized persistent collection of an entity.
2363
     *
2364
     * @param \Doctrine\ORM\PersistentCollection $collection The collection to initialize.
2365
     *
2366
     * @todo Maybe later move to EntityManager#initialize($proxyOrCollection). See DDC-733.
2367
     */
2368 132
    public function loadCollection(PersistentCollection $collection)
2369
    {
2370 132
        $association = $collection->getMapping();
2371 132
        $persister   = $this->getEntityPersister($association->getTargetEntity());
2372
2373 132
        if ($association instanceof OneToManyAssociationMetadata) {
2374 68
            $persister->loadOneToManyCollection($association, $collection->getOwner(), $collection);
2375
        } else {
2376 72
            $persister->loadManyToManyCollection($association, $collection->getOwner(), $collection);
2377
        }
2378
2379 132
        $collection->setInitialized(true);
2380 132
    }
2381
2382
    /**
2383
     * Gets the identity map of the UnitOfWork.
2384
     *
2385
     * @return object[]
2386
     */
2387 1
    public function getIdentityMap()
2388
    {
2389 1
        return $this->identityMap;
2390
    }
2391
2392
    /**
2393
     * Gets the original data of an entity. The original data is the data that was
2394
     * present at the time the entity was reconstituted from the database.
2395
     *
2396
     * @param object $entity
2397
     *
2398
     * @return mixed[]
2399
     */
2400 118
    public function getOriginalEntityData($entity)
2401
    {
2402 118
        $oid = spl_object_id($entity);
2403
2404 118
        return $this->originalEntityData[$oid] ?? [];
2405
    }
2406
2407
    /**
2408
     * @ignore
2409
     *
2410
     * @param object  $entity
2411
     * @param mixed[] $data
2412
     */
2413
    public function setOriginalEntityData($entity, array $data)
2414
    {
2415
        $this->originalEntityData[spl_object_id($entity)] = $data;
2416
    }
2417
2418
    /**
2419
     * INTERNAL:
2420
     * Sets a property value of the original data array of an entity.
2421
     *
2422
     * @ignore
2423
     *
2424
     * @param string $oid
2425
     * @param string $property
2426
     * @param mixed  $value
2427
     */
2428 280
    public function setOriginalEntityProperty($oid, $property, $value)
2429
    {
2430 280
        $this->originalEntityData[$oid][$property] = $value;
2431 280
    }
2432
2433
    /**
2434
     * Gets the identifier of an entity.
2435
     * The returned value is always an array of identifier values. If the entity
2436
     * has a composite identifier then the identifier values are in the same
2437
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
2438
     *
2439
     * @param object $entity
2440
     *
2441
     * @return mixed[] The identifier values.
2442
     */
2443 550
    public function getEntityIdentifier($entity)
2444
    {
2445 550
        return $this->entityIdentifiers[spl_object_id($entity)];
2446
    }
2447
2448
    /**
2449
     * Processes an entity instance to extract their identifier values.
2450
     *
2451
     * @param object $entity The entity instance.
2452
     *
2453
     * @return mixed A scalar value.
2454
     *
2455
     * @throws \Doctrine\ORM\ORMInvalidArgumentException
2456
     */
2457 63
    public function getSingleIdentifierValue($entity)
2458
    {
2459 63
        $class     = $this->em->getClassMetadata(get_class($entity));
2460 63
        $persister = $this->getEntityPersister($class->getClassName());
2461
2462 63
        if ($class->isIdentifierComposite()) {
2463
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2464
        }
2465
2466 63
        $values = $this->isInIdentityMap($entity)
2467 53
            ? $this->getEntityIdentifier($entity)
2468 63
            : $persister->getIdentifier($entity);
2469
2470 63
        return $values[$class->identifier[0]] ?? null;
2471
    }
2472
2473
    /**
2474
     * Tries to find an entity with the given identifier in the identity map of
2475
     * this UnitOfWork.
2476
     *
2477
     * @param mixed|mixed[] $id            The entity identifier to look for.
2478
     * @param string        $rootClassName The name of the root class of the mapped entity hierarchy.
2479
     *
2480
     * @return object|bool Returns the entity with the specified identifier if it exists in
2481
     *                     this UnitOfWork, FALSE otherwise.
2482
     */
2483 499
    public function tryGetById($id, $rootClassName)
2484
    {
2485 499
        $idHash = implode(' ', (array) $id);
2486
2487 499
        return $this->identityMap[$rootClassName][$idHash] ?? false;
2488
    }
2489
2490
    /**
2491
     * Schedules an entity for dirty-checking at commit-time.
2492
     *
2493
     * @param object $entity The entity to schedule for dirty-checking.
2494
     */
2495 5
    public function scheduleForSynchronization($entity)
2496
    {
2497 5
        $rootClassName = $this->em->getClassMetadata(get_class($entity))->getRootClassName();
2498
2499 5
        $this->scheduledForSynchronization[$rootClassName][spl_object_id($entity)] = $entity;
2500 5
    }
2501
2502
    /**
2503
     * Checks whether the UnitOfWork has any pending insertions.
2504
     *
2505
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2506
     */
2507
    public function hasPendingInsertions()
2508
    {
2509
        return ! empty($this->entityInsertions);
2510
    }
2511
2512
    /**
2513
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2514
     * number of entities in the identity map.
2515
     *
2516
     * @return int
2517
     */
2518 1
    public function size()
2519
    {
2520 1
        return \array_sum(\array_map('count', $this->identityMap));
2521
    }
2522
2523
    /**
2524
     * Gets the EntityPersister for an Entity.
2525
     *
2526
     * @param string $entityName The name of the Entity.
2527
     *
2528
     * @return \Doctrine\ORM\Persisters\Entity\EntityPersister
2529
     */
2530 1000
    public function getEntityPersister($entityName)
2531
    {
2532 1000
        if (isset($this->entityPersisters[$entityName])) {
2533 958
            return $this->entityPersisters[$entityName];
2534
        }
2535
2536 1000
        $class = $this->em->getClassMetadata($entityName);
2537
2538
        switch (true) {
2539 1000
            case ($class->inheritanceType === InheritanceType::NONE):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2540 965
                $persister = new BasicEntityPersister($this->em, $class);
2541 965
                break;
2542
2543 374
            case ($class->inheritanceType === InheritanceType::SINGLE_TABLE):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2544 221
                $persister = new SingleTablePersister($this->em, $class);
2545 221
                break;
2546
2547 344
            case ($class->inheritanceType === InheritanceType::JOINED):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2548 344
                $persister = new JoinedSubclassPersister($this->em, $class);
2549 344
                break;
2550
2551
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
2552
                throw new \RuntimeException('No persister found for entity.');
2553
        }
2554
2555 1000
        if ($this->hasCache && $class->getCache()) {
2556 128
            $persister = $this->em->getConfiguration()
2557 128
                ->getSecondLevelCacheConfiguration()
2558 128
                ->getCacheFactory()
2559 128
                ->buildCachedEntityPersister($this->em, $persister, $class);
2560
        }
2561
2562 1000
        $this->entityPersisters[$entityName] = $persister;
2563
2564 1000
        return $this->entityPersisters[$entityName];
2565
    }
2566
2567
    /**
2568
     * Gets a collection persister for a collection-valued association.
2569
     *
2570
     * @return \Doctrine\ORM\Persisters\Collection\CollectionPersister
2571
     */
2572 550
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2573
    {
2574 550
        $role = $association->getCache()
2575 77
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2576 550
            : get_class($association);
2577
2578 550
        if (isset($this->collectionPersisters[$role])) {
2579 425
            return $this->collectionPersisters[$role];
2580
        }
2581
2582 550
        $persister = $association instanceof OneToManyAssociationMetadata
2583 389
            ? new OneToManyPersister($this->em)
2584 550
            : new ManyToManyPersister($this->em);
2585
2586 550
        if ($this->hasCache && $association->getCache()) {
2587 76
            $persister = $this->em->getConfiguration()
2588 76
                ->getSecondLevelCacheConfiguration()
2589 76
                ->getCacheFactory()
2590 76
                ->buildCachedCollectionPersister($this->em, $persister, $association);
2591
        }
2592
2593 550
        $this->collectionPersisters[$role] = $persister;
2594
2595 550
        return $this->collectionPersisters[$role];
2596
    }
2597
2598
    /**
2599
     * INTERNAL:
2600
     * Registers an entity as managed.
2601
     *
2602
     * @param object  $entity The entity.
2603
     * @param mixed[] $id     Map containing identifier field names as key and its associated values.
2604
     * @param mixed[] $data   The original entity data.
2605
     */
2606 273
    public function registerManaged($entity, array $id, array $data)
2607
    {
2608 273
        $isProxy = $entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized();
2609 273
        $oid     = spl_object_id($entity);
2610
2611 273
        $this->entityIdentifiers[$oid]  = $id;
2612 273
        $this->entityStates[$oid]       = self::STATE_MANAGED;
2613 273
        $this->originalEntityData[$oid] = $data;
2614
2615 273
        $this->addToIdentityMap($entity);
2616
2617 267
        if ($entity instanceof NotifyPropertyChanged && ! $isProxy) {
2618 1
            $entity->addPropertyChangedListener($this);
2619
        }
2620 267
    }
2621
2622
    /**
2623
     * INTERNAL:
2624
     * Clears the property changeset of the entity with the given OID.
2625
     *
2626
     * @param string $oid The entity's OID.
2627
     */
2628
    public function clearEntityChangeSet($oid)
2629
    {
2630
        unset($this->entityChangeSets[$oid]);
2631
    }
2632
2633
    /* PropertyChangedListener implementation */
2634
2635
    /**
2636
     * Notifies this UnitOfWork of a property change in an entity.
2637
     *
2638
     * @param object $entity       The entity that owns the property.
2639
     * @param string $propertyName The name of the property that changed.
2640
     * @param mixed  $oldValue     The old value of the property.
2641
     * @param mixed  $newValue     The new value of the property.
2642
     */
2643 3
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
2644
    {
2645 3
        $class = $this->em->getClassMetadata(get_class($entity));
2646
2647 3
        if (! $class->getProperty($propertyName)) {
2648
            return; // ignore non-persistent fields
2649
        }
2650
2651 3
        $oid = spl_object_id($entity);
2652
2653
        // Update changeset and mark entity for synchronization
2654 3
        $this->entityChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2655
2656 3
        if (! isset($this->scheduledForSynchronization[$class->getRootClassName()][$oid])) {
2657 3
            $this->scheduleForSynchronization($entity);
2658
        }
2659 3
    }
2660
2661
    /**
2662
     * Gets the currently scheduled entity insertions in this UnitOfWork.
2663
     *
2664
     * @return object[]
2665
     */
2666 2
    public function getScheduledEntityInsertions()
2667
    {
2668 2
        return $this->entityInsertions;
2669
    }
2670
2671
    /**
2672
     * Gets the currently scheduled entity updates in this UnitOfWork.
2673
     *
2674
     * @return object[]
2675
     */
2676 3
    public function getScheduledEntityUpdates()
2677
    {
2678 3
        return $this->entityUpdates;
2679
    }
2680
2681
    /**
2682
     * Gets the currently scheduled entity deletions in this UnitOfWork.
2683
     *
2684
     * @return object[]
2685
     */
2686 1
    public function getScheduledEntityDeletions()
2687
    {
2688 1
        return $this->entityDeletions;
2689
    }
2690
2691
    /**
2692
     * Gets the currently scheduled complete collection deletions
2693
     *
2694
     * @return Collection[]|object[][]
2695
     */
2696 1
    public function getScheduledCollectionDeletions()
2697
    {
2698 1
        return $this->collectionDeletions;
2699
    }
2700
2701
    /**
2702
     * Gets the currently scheduled collection inserts, updates and deletes.
2703
     *
2704
     * @return Collection[]|object[][]
2705
     */
2706
    public function getScheduledCollectionUpdates()
2707
    {
2708
        return $this->collectionUpdates;
2709
    }
2710
2711
    /**
2712
     * Helper method to initialize a lazy loading proxy or persistent collection.
2713
     *
2714
     * @param object $obj
2715
     */
2716 2
    public function initializeObject($obj)
2717
    {
2718 2
        if ($obj instanceof GhostObjectInterface) {
2719 1
            $obj->initializeProxy();
2720
2721 1
            return;
2722
        }
2723
2724 1
        if ($obj instanceof PersistentCollection) {
2725 1
            $obj->initialize();
2726
        }
2727 1
    }
2728
2729
    /**
2730
     * Helper method to show an object as string.
2731
     *
2732
     * @param object $obj
2733
     *
2734
     * @return string
2735
     */
2736
    private static function objToStr($obj)
2737
    {
2738
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_id($obj);
2739
    }
2740
2741
    /**
2742
     * Marks an entity as read-only so that it will not be considered for updates during UnitOfWork#commit().
2743
     *
2744
     * This operation cannot be undone as some parts of the UnitOfWork now keep gathering information
2745
     * on this object that might be necessary to perform a correct update.
2746
     *
2747
     * @param object $object
2748
     *
2749
     * @throws ORMInvalidArgumentException
2750
     */
2751 3
    public function markReadOnly($object)
2752
    {
2753 3
        if (! is_object($object) || ! $this->isInIdentityMap($object)) {
2754 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2755
        }
2756
2757 2
        $this->readOnlyObjects[spl_object_id($object)] = true;
2758 2
    }
2759
2760
    /**
2761
     * Is this entity read only?
2762
     *
2763
     * @param object $object
2764
     *
2765
     * @return bool
2766
     *
2767
     * @throws ORMInvalidArgumentException
2768
     */
2769
    public function isReadOnly($object)
2770
    {
2771
        if (! is_object($object)) {
2772
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2773
        }
2774
2775
        return isset($this->readOnlyObjects[spl_object_id($object)]);
2776
    }
2777
2778
    /**
2779
     * Perform whatever processing is encapsulated here after completion of the transaction.
2780
     */
2781
    private function afterTransactionComplete()
2782
    {
2783 932
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2784 92
            $persister->afterTransactionComplete();
2785 932
        });
2786 932
    }
2787
2788
    /**
2789
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
2790
     */
2791
    private function afterTransactionRolledBack()
2792
    {
2793 6
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2794
            $persister->afterTransactionRolledBack();
2795 6
        });
2796 6
    }
2797
2798
    /**
2799
     * Performs an action after the transaction.
2800
     */
2801 933
    private function performCallbackOnCachedPersister(callable $callback)
2802
    {
2803 933
        if (! $this->hasCache) {
2804 841
            return;
2805
        }
2806
2807 92
        foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) {
2808 92
            if ($persister instanceof CachedPersister) {
2809 92
                $callback($persister);
2810
            }
2811
        }
2812 92
    }
2813
2814 938
    private function dispatchOnFlushEvent()
2815
    {
2816 938
        if ($this->eventManager->hasListeners(Events::onFlush)) {
2817 4
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2818
        }
2819 938
    }
2820
2821 937
    private function dispatchPostFlushEvent()
2822
    {
2823 937
        if ($this->eventManager->hasListeners(Events::postFlush)) {
2824 5
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2825
        }
2826 936
    }
2827
2828
    /**
2829
     * Verifies if two given entities actually are the same based on identifier comparison
2830
     *
2831
     * @param object $entity1
2832
     * @param object $entity2
2833
     *
2834
     * @return bool
2835
     */
2836 15
    private function isIdentifierEquals($entity1, $entity2)
2837
    {
2838 15
        if ($entity1 === $entity2) {
2839
            return true;
2840
        }
2841
2842 15
        $class     = $this->em->getClassMetadata(get_class($entity1));
2843 15
        $persister = $this->getEntityPersister($class->getClassName());
2844
2845 15
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
2846 11
            return false;
2847
        }
2848
2849 4
        $identifierFlattener = $this->em->getIdentifierFlattener();
2850
2851 4
        $oid1 = spl_object_id($entity1);
2852 4
        $oid2 = spl_object_id($entity2);
2853
2854 4
        $id1 = $this->entityIdentifiers[$oid1]
2855 4
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1));
2856 4
        $id2 = $this->entityIdentifiers[$oid2]
2857 4
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2));
2858
2859 4
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
2860
    }
2861
2862
    /**
2863
     * @throws ORMInvalidArgumentException
2864
     */
2865 935
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2866
    {
2867 935
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2868
2869 935
        $this->nonCascadedNewDetectedEntities = [];
2870
2871 935
        if ($entitiesNeedingCascadePersist) {
2872 4
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
2873 4
                \array_values($entitiesNeedingCascadePersist)
2874
            );
2875
        }
2876 933
    }
2877
2878
    /**
2879
     * This method called by hydrators, and indicates that hydrator totally completed current hydration cycle.
2880
     * Unit of work able to fire deferred events, related to loading events here.
2881
     *
2882
     * @internal should be called internally from object hydrators
2883
     */
2884 821
    public function hydrationComplete()
2885
    {
2886 821
        $this->hydrationCompleteHandler->hydrationComplete();
2887 821
    }
2888
}
2889