Failed Conditions
Push — master ( 2ccf23...d791f7 )
by Michael
10:40
created

UnitOfWork::cascadePersist()   C

Complexity

Conditions 13
Paths 21

Size

Total Lines 53
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 13

Importance

Changes 0
Metric Value
cc 13
eloc 30
nc 21
nop 2
dl 0
loc 53
ccs 30
cts 30
cp 1
crap 13
rs 6.3327
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 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 2259
    public function __construct(EntityManagerInterface $em)
290
    {
291 2259
        $this->em                       = $em;
292 2259
        $this->eventManager             = $em->getEventManager();
293 2259
        $this->listenersInvoker         = new ListenersInvoker($em);
294 2259
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
295 2259
        $this->instantiator             = new Instantiator();
296 2259
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
297 2259
        $this->normalizeIdentifier      = new NormalizeIdentifier();
298 2259
    }
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 1012
    public function commit()
316
    {
317
        // Raise preFlush
318 1012
        if ($this->eventManager->hasListeners(Events::preFlush)) {
319 2
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
320
        }
321
322 1012
        $this->computeChangeSets();
323
324 1010
        if (! ($this->entityInsertions ||
325 154
                $this->entityDeletions ||
326 119
                $this->entityUpdates ||
327 36
                $this->collectionUpdates ||
328 33
                $this->collectionDeletions ||
329 1010
                $this->orphanRemovals)) {
330 21
            $this->dispatchOnFlushEvent();
331 21
            $this->dispatchPostFlushEvent();
332
333 21
            return; // Nothing to do.
334
        }
335
336 1005
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
337
338 1003
        if ($this->orphanRemovals) {
339 15
            foreach ($this->orphanRemovals as $orphan) {
340 15
                $this->remove($orphan);
341
            }
342
        }
343
344 1003
        $this->dispatchOnFlushEvent();
345
346
        // Now we need a commit order to maintain referential integrity
347 1003
        $commitOrder = $this->getCommitOrder();
348
349 1003
        $conn = $this->em->getConnection();
350 1003
        $conn->beginTransaction();
351
352
        try {
353
            // Collection deletions (deletions of complete collections)
354 1003
            foreach ($this->collectionDeletions as $collectionToDelete) {
355 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
356
            }
357
358 1003
            if ($this->entityInsertions) {
359 999
                foreach ($commitOrder as $class) {
360 999
                    $this->executeInserts($class);
361
                }
362
            }
363
364 1002
            if ($this->entityUpdates) {
365 108
                foreach ($commitOrder as $class) {
366 108
                    $this->executeUpdates($class);
367
                }
368
            }
369
370
            // Extra updates that were requested by persisters.
371 998
            if ($this->extraUpdates) {
372 33
                $this->executeExtraUpdates();
373
            }
374
375
            // Collection updates (deleteRows, updateRows, insertRows)
376 998
            foreach ($this->collectionUpdates as $collectionToUpdate) {
377 526
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
378
            }
379
380
            // Entity deletions come last and need to be in reverse commit order
381 998
            if ($this->entityDeletions) {
382 60
                foreach (array_reverse($commitOrder) as $committedEntityName) {
383 60
                    if (! $this->entityDeletions) {
384 34
                        break; // just a performance optimisation
385
                    }
386
387 60
                    $this->executeDeletions($committedEntityName);
388
                }
389
            }
390
391 998
            $conn->commit();
392 10
        } catch (\Throwable $e) {
393 10
            $this->em->close();
394 10
            $conn->rollBack();
395
396 10
            $this->afterTransactionRolledBack();
397
398 10
            throw $e;
399
        }
400
401 998
        $this->afterTransactionComplete();
402
403
        // Take new snapshots from visited collections
404 998
        foreach ($this->visitedCollections as $coll) {
405 525
            $coll->takeSnapshot();
406
        }
407
408 998
        $this->dispatchPostFlushEvent();
409
410
        // Clean up
411 997
        $this->entityInsertions            =
412 997
        $this->entityUpdates               =
413 997
        $this->entityDeletions             =
414 997
        $this->extraUpdates                =
415 997
        $this->entityChangeSets            =
416 997
        $this->collectionUpdates           =
417 997
        $this->collectionDeletions         =
418 997
        $this->visitedCollections          =
419 997
        $this->scheduledForSynchronization =
420 997
        $this->orphanRemovals              = [];
421 997
    }
422
423
    /**
424
     * Computes the changesets of all entities scheduled for insertion.
425
     */
426 1012
    private function computeScheduleInsertsChangeSets()
427
    {
428 1012
        foreach ($this->entityInsertions as $entity) {
429 1003
            $class = $this->em->getClassMetadata(get_class($entity));
430
431 1003
            $this->computeChangeSet($class, $entity);
432
        }
433 1010
    }
434
435
    /**
436
     * Executes any extra updates that have been scheduled.
437
     */
438 33
    private function executeExtraUpdates()
439
    {
440 33
        foreach ($this->extraUpdates as $oid => $update) {
441 33
            list ($entity, $changeset) = $update;
442
443 33
            $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 33
            $this->getEntityPersister(get_class($entity))->update($entity);
449
        }
450
451 33
        $this->extraUpdates = [];
452 33
    }
453
454
    /**
455
     * Gets the changeset for an entity.
456
     *
457
     * @param object $entity
458
     *
459
     * @return mixed[]
460
     */
461 998
    public function & getEntityChangeSet($entity)
462
    {
463 998
        $oid  = spl_object_id($entity);
464 998
        $data = [];
465
466 998
        if (! isset($this->entityChangeSets[$oid])) {
467 2
            return $data;
468
        }
469
470 998
        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 1013
    public function computeChangeSet(ClassMetadata $class, $entity)
507
    {
508 1013
        $oid = spl_object_id($entity);
509
510 1013
        if (isset($this->readOnlyObjects[$oid])) {
511 2
            return;
512
        }
513
514 1013
        if ($class->inheritanceType !== InheritanceType::NONE) {
515 330
            $class = $this->em->getClassMetadata(get_class($entity));
516
        }
517
518 1013
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
519
520 1013
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
521 133
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
522
        }
523
524 1013
        $actualData = [];
525
526 1013
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
527 1013
            $value = $property->getValue($entity);
528
529 1013
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
530 774
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
531 184
                    continue;
532
                }
533
534 771
                $value = $property->wrap($entity, $value, $this->em);
535
536 771
                $property->setValue($entity, $value);
537
538 771
                $actualData[$name] = $value;
539
540 771
                continue;
541
            }
542
543 1013
            if (( ! $class->isIdentifier($name)
544 1013
                    || ! $class->getProperty($name) instanceof FieldMetadata
545 1013
                    || ! $class->getProperty($name)->hasValueGenerator()
546 1013
                    || $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
547 1013
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
548 1013
                $actualData[$name] = $value;
549
            }
550
        }
551
552 1013
        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 1009
            $this->originalEntityData[$oid] = $actualData;
556 1009
            $changeSet                      = [];
557
558 1009
            foreach ($actualData as $propName => $actualValue) {
559 989
                $property = $class->getProperty($propName);
560
561 989
                if (($property instanceof FieldMetadata) ||
562 989
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
563 989
                    $changeSet[$propName] = [null, $actualValue];
564
                }
565
            }
566
567 1009
            $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 250
            $originalData           = $this->originalEntityData[$oid];
572 250
            $isChangeTrackingNotify = $class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY;
573 250
            $changeSet              = ($isChangeTrackingNotify && isset($this->entityChangeSets[$oid]))
574
                ? $this->entityChangeSets[$oid]
575 250
                : [];
576
577 250
            foreach ($actualData as $propName => $actualValue) {
578
                // skip field, its a partially omitted one!
579 240
                if (! (isset($originalData[$propName]) || array_key_exists($propName, $originalData))) {
580 36
                    continue;
581
                }
582
583 240
                $orgValue = $originalData[$propName];
584
585
                // skip if value haven't changed
586 240
                if ($orgValue === $actualValue) {
587 224
                    continue;
588
                }
589
590 106
                $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 106
                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 106
                    case ($property instanceof FieldMetadata):
615 55
                        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 55
                        $changeSet[$propName] = [$orgValue, $actualValue];
622 55
                        break;
623
624 57
                    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 12
                    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 106
                    default:
649
                        // Do nothing
650
                }
651
            }
652
653 250
            if ($changeSet) {
654 81
                $this->entityChangeSets[$oid]   = $changeSet;
655 81
                $this->originalEntityData[$oid] = $actualData;
656 81
                $this->entityUpdates[$oid]      = $entity;
657
            }
658
        }
659
660
        // Look for changes in associations of the entity
661 1013
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
662 1013
            if (! $property instanceof AssociationMetadata) {
663 1013
                continue;
664
            }
665
666 885
            $value = $property->getValue($entity);
667
668 885
            if ($value === null) {
669 627
                continue;
670
            }
671
672 861
            $this->computeAssociationChanges($property, $value);
673
674 853
            if ($property instanceof ManyToManyAssociationMetadata &&
675 853
                $value instanceof PersistentCollection &&
676 853
                ! isset($this->entityChangeSets[$oid]) &&
677 853
                $property->isOwningSide() &&
678 853
                $value->isDirty()) {
679 31
                $this->entityChangeSets[$oid]   = [];
680 31
                $this->originalEntityData[$oid] = $actualData;
681 853
                $this->entityUpdates[$oid]      = $entity;
682
            }
683
        }
684 1005
    }
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 1012
    public function computeChangeSets()
692
    {
693
        // Compute changes for INSERTed entities first. This must always happen.
694 1012
        $this->computeScheduleInsertsChangeSets();
695
696
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
697 1010
        foreach ($this->identityMap as $className => $entities) {
698 445
            $class = $this->em->getClassMetadata($className);
699
700
            // Skip class if instances are read-only
701 445
            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 444
                case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT):
709 442
                    $entitiesToProcess = $entities;
710 442
                    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 444
            foreach ($entitiesToProcess as $entity) {
721
                // Ignore uninitialized proxy objects
722 424
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
723 37
                    continue;
724
                }
725
726
                // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here.
727 422
                $oid = spl_object_id($entity);
728
729 422
                if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
730 444
                    $this->computeChangeSet($class, $entity);
731
                }
732
            }
733
        }
734 1010
    }
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 861
    private function computeAssociationChanges(AssociationMetadata $association, $value)
746
    {
747 861
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
748 30
            return;
749
        }
750
751 860
        if ($value instanceof PersistentCollection && $value->isDirty()) {
752 529
            $coid = spl_object_id($value);
753
754 529
            $this->collectionUpdates[$coid]  = $value;
755 529
            $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 860
        $unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap();
762 860
        $targetEntity   = $association->getTargetEntity();
763 860
        $targetClass    = $this->em->getClassMetadata($targetEntity);
764
765 860
        foreach ($unwrappedValue as $key => $entry) {
766 718
            if (! ($entry instanceof $targetEntity)) {
767 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
768
            }
769
770 710
            $state = $this->getEntityState($entry, self::STATE_NEW);
771
772 710
            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 710
                case self::STATE_NEW:
783 41
                    if (! in_array('persist', $association->getCascade(), true)) {
784 5
                        $this->nonCascadedNewDetectedEntities[\spl_object_id($entry)] = [$association, $entry];
785
786 5
                        break;
787
                    }
788
789 37
                    $this->persistNew($targetClass, $entry);
790 37
                    $this->computeChangeSet($targetClass, $entry);
791
792 37
                    break;
793
794 703
                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 703
                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 710
                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 852
    }
814
815
    /**
816
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
817
     * @param object                              $entity
818
     */
819 1024
    private function persistNew($class, $entity)
820
    {
821 1024
        $oid    = spl_object_id($entity);
822 1024
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
823
824 1024
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
825 135
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
826
        }
827
828 1024
        $generationPlan = $class->getValueGenerationPlan();
829 1024
        $persister      = $this->getEntityPersister($class->getClassName());
830 1024
        $generationPlan->executeImmediate($this->em, $entity);
831
832 1024
        if (! $generationPlan->containsDeferred()) {
833 267
            $id                            = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
834 267
            $this->entityIdentifiers[$oid] = $id;
835
        }
836
837 1024
        $this->entityStates[$oid] = self::STATE_MANAGED;
838
839 1024
        $this->scheduleForInsert($entity);
840 1024
    }
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 15
    public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity) : void
860
    {
861 15
        $oid = spl_object_id($entity);
862
863 15
        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 15
        if ($class->changeTrackingPolicy === ChangeTrackingPolicy::NOTIFY) {
869
            return;
870
        }
871
872 15
        if ($class->inheritanceType !== InheritanceType::NONE) {
873 3
            $class = $this->em->getClassMetadata(get_class($entity));
874
        }
875
876 15
        $actualData = [];
877
878 15
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
879
            switch (true) {
880 15
                case ($property instanceof VersionFieldMetadata):
881
                    // Ignore version field
882
                    break;
883
884 15
                case ($property instanceof FieldMetadata):
885 15
                    if (! $property->isPrimaryKey()
886 15
                        || ! $property->getValueGenerator()
887 15
                        || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY) {
888 15
                        $actualData[$name] = $property->getValue($entity);
889
                    }
890
891 15
                    break;
892
893 11
                case ($property instanceof ToOneAssociationMetadata):
894 9
                    $actualData[$name] = $property->getValue($entity);
895 15
                    break;
896
            }
897
        }
898
899 15
        if (! isset($this->originalEntityData[$oid])) {
900
            throw new \RuntimeException('Cannot call recomputeSingleEntityChangeSet before computeChangeSet on an entity.');
901
        }
902
903 15
        $originalData = $this->originalEntityData[$oid];
904 15
        $changeSet    = [];
905
906 15
        foreach ($actualData as $propName => $actualValue) {
907 15
            $orgValue = $originalData[$propName] ?? null;
908
909 15
            if ($orgValue !== $actualValue) {
910 15
                $changeSet[$propName] = [$orgValue, $actualValue];
911
            }
912
        }
913
914 15
        if ($changeSet) {
915 7
            if (isset($this->entityChangeSets[$oid])) {
916 6
                $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 7
            $this->originalEntityData[$oid] = $actualData;
922
        }
923 15
    }
924
925
    /**
926
     * Executes all entity insertions for entities of the specified type.
927
     */
928 999
    private function executeInserts(ClassMetadata $class) : void
929
    {
930 999
        $className      = $class->getClassName();
931 999
        $persister      = $this->getEntityPersister($className);
932 999
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
933 999
        $generationPlan = $class->getValueGenerationPlan();
934
935 999
        foreach ($this->entityInsertions as $oid => $entity) {
936 999
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
937 853
                continue;
938
            }
939
940 999
            $persister->insert($entity);
941
942 998
            if ($generationPlan->containsDeferred()) {
943
                // Entity has post-insert IDs
944 906
                $oid = spl_object_id($entity);
945 906
                $id  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
946
947 906
                $this->entityIdentifiers[$oid]  = $id;
948 906
                $this->entityStates[$oid]       = self::STATE_MANAGED;
949 906
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
950
951 906
                $this->addToIdentityMap($entity);
952
            }
953
954 998
            unset($this->entityInsertions[$oid]);
955
956 998
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
957 131
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
958
959 998
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
960
            }
961
        }
962 999
    }
963
964
    /**
965
     * Executes all entity updates for entities of the specified type.
966
     *
967
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
968
     */
969 108
    private function executeUpdates($class)
970
    {
971 108
        $className        = $class->getClassName();
972 108
        $persister        = $this->getEntityPersister($className);
973 108
        $preUpdateInvoke  = $this->listenersInvoker->getSubscribedSystems($class, Events::preUpdate);
974 108
        $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
975
976 108
        foreach ($this->entityUpdates as $oid => $entity) {
977 108
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
978 70
                continue;
979
            }
980
981 108
            if ($preUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
982 12
                $this->listenersInvoker->invoke($class, Events::preUpdate, $entity, new PreUpdateEventArgs($entity, $this->em, $this->getEntityChangeSet($entity)), $preUpdateInvoke);
983
984 12
                $this->recomputeSingleEntityChangeSet($class, $entity);
985
            }
986
987 108
            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 78
                $persister->update($entity);
992
            }
993
994 104
            unset($this->entityUpdates[$oid]);
995
996 104
            if ($postUpdateInvoke !== ListenersInvoker::INVOKE_NONE) {
997 104
                $this->listenersInvoker->invoke($class, Events::postUpdate, $entity, new LifecycleEventArgs($entity, $this->em), $postUpdateInvoke);
998
            }
999
        }
1000 104
    }
1001
1002
    /**
1003
     * Executes all entity deletions for entities of the specified type.
1004
     *
1005
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
1006
     */
1007 60
    private function executeDeletions($class)
1008
    {
1009 60
        $className = $class->getClassName();
1010 60
        $persister = $this->getEntityPersister($className);
1011 60
        $invoke    = $this->listenersInvoker->getSubscribedSystems($class, Events::postRemove);
1012
1013 60
        foreach ($this->entityDeletions as $oid => $entity) {
1014 60
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
1015 24
                continue;
1016
            }
1017
1018 60
            $persister->delete($entity);
1019
1020
            unset(
1021 60
                $this->entityDeletions[$oid],
1022 60
                $this->entityIdentifiers[$oid],
1023 60
                $this->originalEntityData[$oid],
1024 60
                $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 60
            if (! $class->isIdentifierComposite()) {
1031 57
                $property = $class->getProperty($class->getSingleIdentifierFieldName());
1032
1033 57
                if ($property instanceof FieldMetadata && $property->hasValueGenerator()) {
1034 50
                    $property->setValue($entity, null);
1035
                }
1036
            }
1037
1038 60
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1039 9
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
1040
1041 60
                $this->listenersInvoker->invoke($class, Events::postRemove, $entity, $eventArgs, $invoke);
1042
            }
1043
        }
1044 59
    }
1045
1046
    /**
1047
     * Gets the commit order.
1048
     *
1049
     * @return ClassMetadata[]
1050
     */
1051 1003
    private function getCommitOrder()
1052
    {
1053 1003
        $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 1003
        $newNodes = [];
1061
1062 1003
        foreach (\array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1063 1003
            $class = $this->em->getClassMetadata(get_class($entity));
1064
1065 1003
            if ($calc->hasNode($class->getClassName())) {
1066 633
                continue;
1067
            }
1068
1069 1003
            $calc->addNode($class->getClassName(), $class);
1070
1071 1003
            $newNodes[] = $class;
1072
        }
1073
1074
        // Calculate dependencies for new nodes
1075 1003
        while ($class = array_pop($newNodes)) {
1076 1003
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1077 1003
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1078 1003
                    continue;
1079
                }
1080
1081 833
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1082
1083 833
                if (! $calc->hasNode($targetClass->getClassName())) {
1084 644
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1085
1086 644
                    $newNodes[] = $targetClass;
1087
                }
1088
1089 833
                $weight = ! array_filter(
1090 833
                    $property->getJoinColumns(),
1091 833
                    function (JoinColumnMetadata $joinColumn) {
1092 833
                        return $joinColumn->isNullable();
1093 833
                    }
1094
                );
1095
1096 833
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight);
1097
1098
                // If the target class has mapped subclasses, these share the same dependency.
1099 833
                if (! $targetClass->getSubClasses()) {
1100 828
                    continue;
1101
                }
1102
1103 233
                foreach ($targetClass->getSubClasses() as $subClassName) {
1104 233
                    $targetSubClass = $this->em->getClassMetadata($subClassName);
1105
1106 233
                    if (! $calc->hasNode($subClassName)) {
1107 205
                        $calc->addNode($targetSubClass->getClassName(), $targetSubClass);
1108
1109 205
                        $newNodes[] = $targetSubClass;
1110
                    }
1111
1112 233
                    $calc->addDependency($targetSubClass->getClassName(), $class->getClassName(), 1);
1113
                }
1114
            }
1115
        }
1116
1117 1003
        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 1025
    public function scheduleForInsert($entity)
1130
    {
1131 1025
        $oid = spl_object_id($entity);
1132
1133 1025
        if (isset($this->entityUpdates[$oid])) {
1134
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1135
        }
1136
1137 1025
        if (isset($this->entityDeletions[$oid])) {
1138 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1139
        }
1140 1025
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1141 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1142
        }
1143
1144 1025
        if (isset($this->entityInsertions[$oid])) {
1145 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1146
        }
1147
1148 1025
        $this->entityInsertions[$oid] = $entity;
1149
1150 1025
        if (isset($this->entityIdentifiers[$oid])) {
1151 267
            $this->addToIdentityMap($entity);
1152
        }
1153
1154 1025
        if ($entity instanceof NotifyPropertyChanged) {
1155 5
            $entity->addPropertyChangedListener($this);
1156
        }
1157 1025
    }
1158
1159
    /**
1160
     * Checks whether an entity is scheduled for insertion.
1161
     *
1162
     * @param object $entity
1163
     *
1164
     * @return bool
1165
     */
1166 621
    public function isScheduledForInsert($entity)
1167
    {
1168 621
        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 33
    public function scheduleExtraUpdate($entity, array $changeset) : void
1209
    {
1210 33
        $oid         = spl_object_id($entity);
1211 33
        $extraUpdate = [$entity, $changeset];
1212
1213 33
        if (isset($this->extraUpdates[$oid])) {
1214 1
            [$unused, $changeset2] = $this->extraUpdates[$oid];
1215
1216 1
            $extraUpdate = [$entity, $changeset + $changeset2];
1217
        }
1218
1219 33
        $this->extraUpdates[$oid] = $extraUpdate;
1220 33
    }
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 63
    public function scheduleForDelete($entity)
1253
    {
1254 63
        $oid = spl_object_id($entity);
1255
1256 63
        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 63
        if (! $this->isInIdentityMap($entity)) {
1267 1
            return;
1268
        }
1269
1270 62
        $this->removeFromIdentityMap($entity);
1271
1272 62
        unset($this->entityUpdates[$oid]);
1273
1274 62
        if (! isset($this->entityDeletions[$oid])) {
1275 62
            $this->entityDeletions[$oid] = $entity;
1276 62
            $this->entityStates[$oid]    = self::STATE_REMOVED;
1277
        }
1278 62
    }
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 1093
    public function addToIdentityMap($entity)
1325
    {
1326 1093
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1327 1093
        $identifier    = $this->entityIdentifiers[spl_object_id($entity)];
1328
1329 1093
        if (empty($identifier) || in_array(null, $identifier, true)) {
1330 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
1331
        }
1332
1333 1087
        $idHash    = implode(' ', $identifier);
1334 1087
        $className = $classMetadata->getRootClassName();
1335
1336 1087
        if (isset($this->identityMap[$className][$idHash])) {
1337 32
            return false;
1338
        }
1339
1340 1087
        $this->identityMap[$className][$idHash] = $entity;
1341
1342 1087
        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 1033
    public function getEntityState($entity, $assume = null)
1357
    {
1358 1033
        $oid = spl_object_id($entity);
1359
1360 1033
        if (isset($this->entityStates[$oid])) {
1361 750
            return $this->entityStates[$oid];
1362
        }
1363
1364 1028
        if ($assume !== null) {
1365 1025
            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 62
    public function removeFromIdentityMap($entity)
1443
    {
1444 62
        $oid           = spl_object_id($entity);
1445 62
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1446 62
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1447
1448 62
        if ($idHash === '') {
1449
            throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'remove from identity map');
1450
        }
1451
1452 62
        $className = $classMetadata->getRootClassName();
1453
1454 62
        if (isset($this->identityMap[$className][$idHash])) {
1455 62
            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 62
            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 144
    public function isInIdentityMap($entity)
1508
    {
1509 144
        $oid = spl_object_id($entity);
1510
1511 144
        if (empty($this->entityIdentifiers[$oid])) {
1512 23
            return false;
1513
        }
1514
1515 130
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1516 130
        $idHash        = implode(' ', $this->entityIdentifiers[$oid]);
1517
1518 130
        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 1025
    public function persist($entity)
1543
    {
1544 1025
        $visited = [];
1545
1546 1025
        $this->doPersist($entity, $visited);
1547 1018
    }
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 1025
    private function doPersist($entity, array &$visited)
1562
    {
1563 1025
        $oid = spl_object_id($entity);
1564
1565 1025
        if (isset($visited[$oid])) {
1566 109
            return; // Prevent infinite recursion
1567
        }
1568
1569 1025
        $visited[$oid] = $entity; // Mark visited
1570
1571 1025
        $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 1025
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1578
1579
        switch ($entityState) {
1580 1025
            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 220
                if ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_EXPLICIT) {
1583 2
                    $this->scheduleForSynchronization($entity);
1584
                }
1585 220
                break;
1586
1587 1025
            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 1024
                $this->persistNew($class, $entity);
1589 1024
                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 1025
        $this->cascadePersist($entity, $visited);
1610 1018
    }
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 62
    public function remove($entity)
1618
    {
1619 62
        $visited = [];
1620
1621 62
        $this->doRemove($entity, $visited);
1622 62
    }
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 62
    private function doRemove($entity, array &$visited)
1637
    {
1638 62
        $oid = spl_object_id($entity);
1639
1640 62
        if (isset($visited[$oid])) {
1641 1
            return; // Prevent infinite recursion
1642
        }
1643
1644 62
        $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 62
        $this->cascadeRemove($entity, $visited);
1649
1650 62
        $class       = $this->em->getClassMetadata(get_class($entity));
1651 62
        $entityState = $this->getEntityState($entity);
1652
1653
        switch ($entityState) {
1654 62
            case self::STATE_NEW:
1655 62
            case self::STATE_REMOVED:
1656
                // nothing to do
1657 2
                break;
1658
1659 62
            case self::STATE_MANAGED:
1660 62
                $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preRemove);
1661
1662 62
                if ($invoke !== ListenersInvoker::INVOKE_NONE) {
1663 8
                    $this->listenersInvoker->invoke($class, Events::preRemove, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
1664
                }
1665
1666 62
                $this->scheduleForDelete($entity);
1667 62
                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 62
    }
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 15
    public function refresh($entity)
1687
    {
1688 15
        $visited = [];
1689
1690 15
        $this->doRefresh($entity, $visited);
1691 15
    }
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 15
    private function doRefresh($entity, array &$visited)
1702
    {
1703 15
        $oid = spl_object_id($entity);
1704
1705 15
        if (isset($visited[$oid])) {
1706
            return; // Prevent infinite recursion
1707
        }
1708
1709 15
        $visited[$oid] = $entity; // mark visited
1710
1711 15
        $class = $this->em->getClassMetadata(get_class($entity));
1712
1713 15
        if ($this->getEntityState($entity) !== self::STATE_MANAGED) {
1714
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1715
        }
1716
1717 15
        $this->getEntityPersister($class->getClassName())->refresh(
1718 15
            array_combine($class->getIdentifierFieldNames(), $this->entityIdentifiers[$oid]),
1719 15
            $entity
1720
        );
1721
1722 15
        $this->cascadeRefresh($entity, $visited);
1723 15
    }
1724
1725
    /**
1726
     * Cascades a refresh operation to associated entities.
1727
     *
1728
     * @param object   $entity
1729
     * @param object[] $visited
1730
     */
1731 15
    private function cascadeRefresh($entity, array &$visited)
1732
    {
1733 15
        $class = $this->em->getClassMetadata(get_class($entity));
1734
1735 15
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1736 15
            if (! ($association instanceof AssociationMetadata && in_array('refresh', $association->getCascade(), true))) {
1737 15
                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 15
    }
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 1025
    private function cascadePersist($entity, array &$visited)
1774
    {
1775 1025
        $class = $this->em->getClassMetadata(get_class($entity));
1776
1777 1025
        if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1778
            // nothing to do - proxy is not initialized, therefore we don't do anything with it
1779 1
            return;
1780
        }
1781
1782 1025
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1783 1025
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade(), true))) {
1784 1025
                continue;
1785
            }
1786
1787
            /** @var AssociationMetadata $association */
1788 648
            $relatedEntities = $association->getValue($entity);
1789 648
            $targetEntity    = $association->getTargetEntity();
1790
1791
            switch (true) {
1792 648
                case ($relatedEntities instanceof PersistentCollection):
1793
                    // Unwrap so that foreach() does not initialize
1794 13
                    $relatedEntities = $relatedEntities->unwrap();
1795
                    // break; is commented intentionally!
1796
1797 648
                case ($relatedEntities instanceof Collection):
1798 586
                case (is_array($relatedEntities)):
1799 544
                    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 541
                    foreach ($relatedEntities as $relatedEntity) {
1808 283
                        $this->doPersist($relatedEntity, $visited);
1809
                    }
1810
1811 541
                    break;
1812
1813 576
                case ($relatedEntities !== null):
1814 245
                    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 241
                    $this->doPersist($relatedEntities, $visited);
1823 241
                    break;
1824
1825 642
                default:
1826
                    // Do nothing
1827
            }
1828
        }
1829 1018
    }
1830
1831
    /**
1832
     * Cascades the delete operation to associated entities.
1833
     *
1834
     * @param object   $entity
1835
     * @param object[] $visited
1836
     */
1837 62
    private function cascadeRemove($entity, array &$visited)
1838
    {
1839 62
        $entitiesToCascade = [];
1840 62
        $class             = $this->em->getClassMetadata(get_class($entity));
1841
1842 62
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1843 62
            if (! ($association instanceof AssociationMetadata && in_array('remove', $association->getCascade(), true))) {
1844 62
                continue;
1845
            }
1846
1847 25
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1848 6
                $entity->initializeProxy();
1849
            }
1850
1851 25
            $relatedEntities = $association->getValue($entity);
1852
1853
            switch (true) {
1854 25
                case ($relatedEntities instanceof Collection):
1855 18
                case (\is_array($relatedEntities)):
1856
                    // If its a PersistentCollection initialization is intended! No unwrap!
1857 20
                    foreach ($relatedEntities as $relatedEntity) {
1858 10
                        $entitiesToCascade[] = $relatedEntity;
1859
                    }
1860 20
                    break;
1861
1862 18
                case ($relatedEntities !== null):
1863 7
                    $entitiesToCascade[] = $relatedEntities;
1864 7
                    break;
1865
1866 25
                default:
1867
                    // Do nothing
1868
            }
1869
        }
1870
1871 62
        foreach ($entitiesToCascade as $relatedEntity) {
1872 16
            $this->doRemove($relatedEntity, $visited);
1873
        }
1874 62
    }
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 10
    public function lock($entity, $lockMode, $lockVersion = null)
1889
    {
1890 10
        if ($entity === null) {
1891 1
            throw new \InvalidArgumentException('No entity passed to UnitOfWork#lock().');
1892
        }
1893
1894 9
        if ($this->getEntityState($entity, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1895 1
            throw ORMInvalidArgumentException::entityNotManaged($entity);
1896
        }
1897
1898 8
        $class = $this->em->getClassMetadata(get_class($entity));
1899
1900
        switch (true) {
1901 8
            case $lockMode === LockMode::OPTIMISTIC:
1902 6
                if (! $class->isVersioned()) {
1903 2
                    throw OptimisticLockException::notVersioned($class->getClassName());
1904
                }
1905
1906 4
                if ($lockVersion === null) {
1907
                    return;
1908
                }
1909
1910 4
                if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
1911 1
                    $entity->initializeProxy();
1912
                }
1913
1914 4
                $entityVersion = $class->versionProperty->getValue($entity);
1915
1916 4
                if ($entityVersion !== $lockVersion) {
1917 2
                    throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion);
1918
                }
1919
1920 2
                break;
1921
1922 2
            case $lockMode === LockMode::NONE:
1923 2
            case $lockMode === LockMode::PESSIMISTIC_READ:
1924 1
            case $lockMode === LockMode::PESSIMISTIC_WRITE:
1925 2
                if (! $this->em->getConnection()->isTransactionActive()) {
1926 2
                    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 2
    }
1941
1942
    /**
1943
     * Clears the UnitOfWork.
1944
     */
1945 1207
    public function clear()
1946
    {
1947 1207
        $this->entityPersisters               =
1948 1207
        $this->collectionPersisters           =
1949 1207
        $this->eagerLoadingEntities           =
1950 1207
        $this->identityMap                    =
1951 1207
        $this->entityIdentifiers              =
1952 1207
        $this->originalEntityData             =
1953 1207
        $this->entityChangeSets               =
1954 1207
        $this->entityStates                   =
1955 1207
        $this->scheduledForSynchronization    =
1956 1207
        $this->entityInsertions               =
1957 1207
        $this->entityUpdates                  =
1958 1207
        $this->entityDeletions                =
1959 1207
        $this->collectionDeletions            =
1960 1207
        $this->collectionUpdates              =
1961 1207
        $this->extraUpdates                   =
1962 1207
        $this->readOnlyObjects                =
1963 1207
        $this->visitedCollections             =
1964 1207
        $this->nonCascadedNewDetectedEntities =
1965 1207
        $this->orphanRemovals                 = [];
1966 1207
    }
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 111
    public function cancelOrphanRemoval($entity)
1992
    {
1993 111
        unset($this->orphanRemovals[spl_object_id($entity)]);
1994 111
    }
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 667
    public function newInstance(ClassMetadata $class)
2029
    {
2030 667
        $entity = $this->instantiator->instantiate($class->getClassName());
2031
2032 667
        if ($entity instanceof EntityManagerAware) {
2033 5
            $entity->injectEntityManager($this->em, $class);
2034
        }
2035
2036 667
        return $entity;
2037
    }
2038
2039
    /**
2040
     * INTERNAL:
2041
     * Creates an entity. Used for reconstitution of persistent entities.
2042
     *
2043
     * {@internal 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 805
    public function createEntity($className, array $data, &$hints = [])
2056
    {
2057 805
        $class  = $this->em->getClassMetadata($className);
2058 805
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
2059 805
        $idHash = implode(' ', $id);
2060
2061 805
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
2062 306
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
2063 306
            $oid    = spl_object_id($entity);
2064
2065 306
            if (isset($hints[Query::HINT_REFRESH], $hints[Query::HINT_REFRESH_ENTITY])) {
2066 66
                $unmanagedProxy = $hints[Query::HINT_REFRESH_ENTITY];
2067 66
                if ($unmanagedProxy !== $entity
2068 66
                    && $unmanagedProxy instanceof GhostObjectInterface
2069 66
                    && $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 5
                    $entity = $unmanagedProxy;
2074
                }
2075
            }
2076
2077 306
            if ($entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized()) {
2078 21
                $entity->setProxyInitializer(null);
2079
2080 21
                if ($entity instanceof NotifyPropertyChanged) {
2081 21
                    $entity->addPropertyChangedListener($this);
2082
                }
2083
            } else {
2084 292
                if (! isset($hints[Query::HINT_REFRESH])
2085 292
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2086 229
                    return $entity;
2087
                }
2088
            }
2089
2090
            // inject EntityManager upon refresh.
2091 104
            if ($entity instanceof EntityManagerAware) {
2092 3
                $entity->injectEntityManager($this->em, $class);
2093
            }
2094
2095 104
            $this->originalEntityData[$oid] = $data;
2096
        } else {
2097 664
            $entity = $this->newInstance($class);
2098 664
            $oid    = spl_object_id($entity);
2099
2100 664
            $this->entityIdentifiers[$oid]  = $id;
2101 664
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2102 664
            $this->originalEntityData[$oid] = $data;
2103
2104 664
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
2105
        }
2106
2107 697
        if ($entity instanceof NotifyPropertyChanged) {
2108 3
            $entity->addPropertyChangedListener($this);
2109
        }
2110
2111 697
        foreach ($data as $field => $value) {
2112 697
            $property = $class->getProperty($field);
2113
2114 697
            if ($property instanceof FieldMetadata) {
2115 697
                $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 697
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2121
2122 697
        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 697
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2128 34
            return $entity;
2129
        }
2130
2131 663
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
2132 663
            if (! ($association instanceof AssociationMetadata)) {
2133 663
                continue;
2134
            }
2135
2136
            // Check if the association is not among the fetch-joined associations already.
2137 572
            if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2138 245
                continue;
2139
            }
2140
2141 549
            $targetEntity = $association->getTargetEntity();
2142 549
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2143
2144 549
            if ($association instanceof ToManyAssociationMetadata) {
2145
                // Ignore if its a cached collection
2146 469
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2147 469
                    $association->getValue($entity) instanceof PersistentCollection) {
2148
                    continue;
2149
                }
2150
2151 469
                $hasDataField = isset($data[$field]);
2152
2153
                // use the given collection
2154 469
                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 469
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2166
2167 469
                $pColl->setInitialized($hasDataField);
2168
2169 469
                $association->setValue($entity, $pColl);
2170
2171 469
                if ($association->getFetchMode() === FetchMode::EAGER) {
2172 4
                    $this->loadCollection($pColl);
2173 4
                    $pColl->takeSnapshot();
2174
                }
2175
2176 469
                $this->originalEntityData[$oid][$field] = $pColl;
2177
2178 469
                continue;
2179
            }
2180
2181 476
            if (! $association->isOwningSide()) {
2182
                // use the given entity association
2183 65
                if (isset($data[$field]) && is_object($data[$field]) &&
2184 65
                    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 62
                $persister = $this->getEntityPersister($targetEntity);
2197
2198 62
                $association->setValue($entity, $persister->loadToOneEntity($association, $entity));
2199
2200 62
                continue;
2201
            }
2202
2203
            // use the entity association
2204 476
            if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_id($data[$field])])) {
2205 38
                $association->setValue($entity, $data[$field]);
2206
2207 38
                $this->originalEntityData[$oid][$field] = $data[$field];
2208
2209 38
                continue;
2210
            }
2211
2212 469
            $associatedId = [];
2213
2214
            // TODO: Is this even computed right in all cases of composite keys?
2215 469
            foreach ($association->getJoinColumns() as $joinColumn) {
2216
                /** @var JoinColumnMetadata $joinColumn */
2217 469
                $joinColumnName  = $joinColumn->getColumnName();
2218 469
                $joinColumnValue = $data[$joinColumnName] ?? null;
2219 469
                $targetField     = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
2220
2221 469
                if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) {
2222
                    // the missing key is part of target's entity primary key
2223 274
                    $associatedId = [];
2224
2225 274
                    continue;
2226
                }
2227
2228 284
                $associatedId[$targetField] = $joinColumnValue;
2229
            }
2230
2231 469
            if (! $associatedId) {
2232
                // Foreign key is NULL
2233 274
                $association->setValue($entity, null);
2234 274
                $this->originalEntityData[$oid][$field] = null;
2235
2236 274
                continue;
2237
            }
2238
2239
            // @todo guilhermeblanco Can we remove the need of this somehow?
2240 284
            if (! isset($hints['fetchMode'][$class->getClassName()][$field])) {
2241 281
                $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 284
            $relatedIdHash = implode(' ', $associatedId);
2249
2250
            switch (true) {
2251 284
                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 166
                    $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 166
                    if (! $targetClass->isIdentifierComposite() &&
2258 166
                        $newValue instanceof GhostObjectInterface &&
2259 166
                        isset($hints[self::HINT_DEFEREAGERLOAD]) &&
2260 166
                        $hints['fetchMode'][$class->getClassName()][$field] === FetchMode::EAGER &&
2261 166
                        ! $newValue->isProxyInitialized()
2262
                    ) {
2263
                        $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($associatedId);
2264
                    }
2265
2266 166
                    break;
2267
2268 192
                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 30
                    $persister = $this->getEntityPersister($targetEntity);
2273 30
                    $newValue  = $persister->loadToOneEntity($association, $entity, $associatedId);
2274 30
                    break;
2275
2276
                default:
2277
                    // Proxies do not carry any kind of original entity data until they're fully loaded/initialized
2278 164
                    $managedData = [];
2279
2280 164
                    $normalizedAssociatedId = $this->normalizeIdentifier->__invoke(
2281 164
                        $this->em,
2282 164
                        $targetClass,
2283 164
                        $associatedId
2284
                    );
2285
2286
                    switch (true) {
2287
                        // We are negating the condition here. Other cases will assume it is valid!
2288 164
                        case ($hints['fetchMode'][$class->getClassName()][$field] !== FetchMode::EAGER):
2289 157
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2290 157
                            break;
2291
2292
                        // Deferred eager load only works for single identifier classes
2293 7
                        case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite()):
2294
                            // TODO: Is there a faster approach?
2295 7
                            $this->eagerLoadingEntities[$targetClass->getRootClassName()][$relatedIdHash] = current($normalizedAssociatedId);
2296
2297 7
                            $newValue = $this->em->getProxyFactory()->getProxy($targetClass, $normalizedAssociatedId);
2298 7
                            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 164
                    $this->registerManaged($newValue, $associatedId, $managedData);
2310
2311 164
                    break;
2312
            }
2313
2314 284
            $this->originalEntityData[$oid][$field] = $newValue;
2315 284
            $association->setValue($entity, $newValue);
2316
2317 284
            if ($association->getInversedBy()
2318 284
                && $association instanceof OneToOneAssociationMetadata
2319
                // @TODO refactor this
2320
                // we don't want to set any values in un-initialized proxies
2321
                && ! (
2322 56
                    $newValue instanceof GhostObjectInterface
2323 284
                    && ! $newValue->isProxyInitialized()
2324
                )
2325
            ) {
2326 19
                $inverseAssociation = $targetClass->getProperty($association->getInversedBy());
2327
2328 284
                $inverseAssociation->setValue($newValue, $entity);
2329
            }
2330
        }
2331
2332
        // defer invoking of postLoad event to hydration complete step
2333 663
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2334
2335 663
        return $entity;
2336
    }
2337
2338 864
    public function triggerEagerLoads()
2339
    {
2340 864
        if (! $this->eagerLoadingEntities) {
2341 864
            return;
2342
        }
2343
2344
        // avoid infinite recursion
2345 7
        $eagerLoadingEntities       = $this->eagerLoadingEntities;
2346 7
        $this->eagerLoadingEntities = [];
2347
2348 7
        foreach ($eagerLoadingEntities as $entityName => $ids) {
2349 7
            if (! $ids) {
2350
                continue;
2351
            }
2352
2353 7
            $class = $this->em->getClassMetadata($entityName);
2354
2355 7
            $this->getEntityPersister($entityName)->loadAll(
2356 7
                array_combine($class->identifier, [array_values($ids)])
2357
            );
2358
        }
2359 7
    }
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 138
    public function loadCollection(PersistentCollection $collection)
2369
    {
2370 138
        $association = $collection->getMapping();
2371 138
        $persister   = $this->getEntityPersister($association->getTargetEntity());
2372
2373 138
        if ($association instanceof OneToManyAssociationMetadata) {
2374 73
            $persister->loadOneToManyCollection($association, $collection->getOwner(), $collection);
2375
        } else {
2376 75
            $persister->loadManyToManyCollection($association, $collection->getOwner(), $collection);
2377
        }
2378
2379 138
        $collection->setInitialized(true);
2380 138
    }
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 120
    public function getOriginalEntityData($entity)
2401
    {
2402 120
        $oid = spl_object_id($entity);
2403
2404 120
        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 303
    public function setOriginalEntityProperty($oid, $property, $value)
2429
    {
2430 303
        $this->originalEntityData[$oid][$property] = $value;
2431 303
    }
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 563
    public function getEntityIdentifier($entity)
2444
    {
2445 563
        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 67
    public function getSingleIdentifierValue($entity)
2458
    {
2459 67
        $class     = $this->em->getClassMetadata(get_class($entity));
2460 67
        $persister = $this->getEntityPersister($class->getClassName());
2461
2462 67
        if ($class->isIdentifierComposite()) {
2463
            throw ORMInvalidArgumentException::invalidCompositeIdentifier();
2464
        }
2465
2466 67
        $values = $this->isInIdentityMap($entity)
2467 55
            ? $this->getEntityIdentifier($entity)
2468 67
            : $persister->getIdentifier($entity);
2469
2470 67
        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 537
    public function tryGetById($id, $rootClassName)
2484
    {
2485 537
        $idHash = implode(' ', (array) $id);
2486
2487 537
        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 1082
    public function getEntityPersister($entityName)
2531
    {
2532 1082
        if (isset($this->entityPersisters[$entityName])) {
2533 1025
            return $this->entityPersisters[$entityName];
2534
        }
2535
2536 1082
        $class = $this->em->getClassMetadata($entityName);
2537
2538
        switch (true) {
2539 1082
            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 1041
                $persister = new BasicEntityPersister($this->em, $class);
2541 1041
                break;
2542
2543 382
            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 223
                $persister = new SingleTablePersister($this->em, $class);
2545 223
                break;
2546
2547 352
            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 352
                $persister = new JoinedSubclassPersister($this->em, $class);
2549 352
                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 1082
        if ($this->hasCache && $class->getCache()) {
2556 130
            $persister = $this->em->getConfiguration()
2557 130
                ->getSecondLevelCacheConfiguration()
2558 130
                ->getCacheFactory()
2559 130
                ->buildCachedEntityPersister($this->em, $persister, $class);
2560
        }
2561
2562 1082
        $this->entityPersisters[$entityName] = $persister;
2563
2564 1082
        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 565
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2573
    {
2574 565
        $role = $association->getCache()
2575 78
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2576 565
            : get_class($association);
2577
2578 565
        if (isset($this->collectionPersisters[$role])) {
2579 432
            return $this->collectionPersisters[$role];
2580
        }
2581
2582 565
        $persister = $association instanceof OneToManyAssociationMetadata
2583 402
            ? new OneToManyPersister($this->em)
2584 565
            : new ManyToManyPersister($this->em);
2585
2586 565
        if ($this->hasCache && $association->getCache()) {
2587 77
            $persister = $this->em->getConfiguration()
2588 77
                ->getSecondLevelCacheConfiguration()
2589 77
                ->getCacheFactory()
2590 77
                ->buildCachedCollectionPersister($this->em, $persister, $association);
2591
        }
2592
2593 565
        $this->collectionPersisters[$role] = $persister;
2594
2595 565
        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 292
    public function registerManaged($entity, array $id, array $data)
2607
    {
2608 292
        $isProxy = $entity instanceof GhostObjectInterface && ! $entity->isProxyInitialized();
2609 292
        $oid     = spl_object_id($entity);
2610
2611 292
        $this->entityIdentifiers[$oid]  = $id;
2612 292
        $this->entityStates[$oid]       = self::STATE_MANAGED;
2613 292
        $this->originalEntityData[$oid] = $data;
2614
2615 292
        $this->addToIdentityMap($entity);
2616
2617 286
        if ($entity instanceof NotifyPropertyChanged && ! $isProxy) {
2618 1
            $entity->addPropertyChangedListener($this);
2619
        }
2620 286
    }
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 6
    public function markReadOnly($object)
2752
    {
2753 6
        if (! is_object($object) || ! $this->isInIdentityMap($object)) {
2754 1
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2755
        }
2756
2757 5
        $this->readOnlyObjects[spl_object_id($object)] = true;
2758 5
    }
2759
2760
    /**
2761
     * Is this entity read only?
2762
     *
2763
     * @param object $object
2764
     *
2765
     * @return bool
2766
     *
2767
     * @throws ORMInvalidArgumentException
2768
     */
2769 3
    public function isReadOnly($object)
2770
    {
2771 3
        if (! is_object($object)) {
2772
            throw ORMInvalidArgumentException::readOnlyRequiresManagedEntity($object);
2773
        }
2774
2775 3
        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 998
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2784 94
            $persister->afterTransactionComplete();
2785 998
        });
2786 998
    }
2787
2788
    /**
2789
     * Perform whatever processing is encapsulated here after completion of the rolled-back.
2790
     */
2791
    private function afterTransactionRolledBack()
2792
    {
2793 10
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2794
            $persister->afterTransactionRolledBack();
2795 10
        });
2796 10
    }
2797
2798
    /**
2799
     * Performs an action after the transaction.
2800
     */
2801 1003
    private function performCallbackOnCachedPersister(callable $callback)
2802
    {
2803 1003
        if (! $this->hasCache) {
2804 909
            return;
2805
        }
2806
2807 94
        foreach (array_merge($this->entityPersisters, $this->collectionPersisters) as $persister) {
2808 94
            if ($persister instanceof CachedPersister) {
2809 94
                $callback($persister);
2810
            }
2811
        }
2812 94
    }
2813
2814 1008
    private function dispatchOnFlushEvent()
2815
    {
2816 1008
        if ($this->eventManager->hasListeners(Events::onFlush)) {
2817 4
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2818
        }
2819 1008
    }
2820
2821 1003
    private function dispatchPostFlushEvent()
2822
    {
2823 1003
        if ($this->eventManager->hasListeners(Events::postFlush)) {
2824 5
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2825
        }
2826 1002
    }
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 17
    private function isIdentifierEquals($entity1, $entity2)
2837
    {
2838 17
        if ($entity1 === $entity2) {
2839
            return true;
2840
        }
2841
2842 17
        $class     = $this->em->getClassMetadata(get_class($entity1));
2843 17
        $persister = $this->getEntityPersister($class->getClassName());
2844
2845 17
        if ($class !== $this->em->getClassMetadata(get_class($entity2))) {
2846 11
            return false;
2847
        }
2848
2849 6
        $identifierFlattener = $this->em->getIdentifierFlattener();
2850
2851 6
        $oid1 = spl_object_id($entity1);
2852 6
        $oid2 = spl_object_id($entity2);
2853
2854 6
        $id1 = $this->entityIdentifiers[$oid1]
2855 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity1));
2856 6
        $id2 = $this->entityIdentifiers[$oid2]
2857 6
            ?? $identifierFlattener->flattenIdentifier($class, $persister->getIdentifier($entity2));
2858
2859 6
        return $id1 === $id2 || implode(' ', $id1) === implode(' ', $id2);
2860
    }
2861
2862
    /**
2863
     * @throws ORMInvalidArgumentException
2864
     */
2865 1005
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2866
    {
2867 1005
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2868
2869 1005
        $this->nonCascadedNewDetectedEntities = [];
2870
2871 1005
        if ($entitiesNeedingCascadePersist) {
2872 4
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
2873 4
                \array_values($entitiesNeedingCascadePersist)
2874
            );
2875
        }
2876 1003
    }
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 879
    public function hydrationComplete()
2885
    {
2886 879
        $this->hydrationCompleteHandler->hydrationComplete();
2887 879
    }
2888
}
2889