Completed
Push — master ( a0071b...e33605 )
by Michael
12s
created

lib/Doctrine/ORM/UnitOfWork.php (5 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM;
6
7
use Doctrine\Common\Collections\Collection;
8
use Doctrine\Common\NotifyPropertyChanged;
9
use Doctrine\Common\PropertyChangedListener;
10
use Doctrine\DBAL\LockMode;
11
use Doctrine\Instantiator\Instantiator;
12
use Doctrine\ORM\Cache\Persister\CachedPersister;
13
use Doctrine\ORM\Event\LifecycleEventArgs;
14
use Doctrine\ORM\Event\ListenersInvoker;
15
use Doctrine\ORM\Event\OnFlushEventArgs;
16
use Doctrine\ORM\Event\PostFlushEventArgs;
17
use Doctrine\ORM\Event\PreFlushEventArgs;
18
use Doctrine\ORM\Event\PreUpdateEventArgs;
19
use Doctrine\ORM\Internal\HydrationCompleteHandler;
20
use Doctrine\ORM\Mapping\AssociationMetadata;
21
use Doctrine\ORM\Mapping\ChangeTrackingPolicy;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use Doctrine\ORM\Mapping\FetchMode;
24
use Doctrine\ORM\Mapping\FieldMetadata;
25
use Doctrine\ORM\Mapping\GeneratorType;
26
use Doctrine\ORM\Mapping\InheritanceType;
27
use Doctrine\ORM\Mapping\JoinColumnMetadata;
28
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
29
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
30
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
31
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
32
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
33
use Doctrine\ORM\Mapping\VersionFieldMetadata;
34
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
35
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
36
use Doctrine\ORM\Persisters\Collection\OneToManyPersister;
37
use Doctrine\ORM\Persisters\Entity\BasicEntityPersister;
38
use Doctrine\ORM\Persisters\Entity\EntityPersister;
39
use Doctrine\ORM\Persisters\Entity\JoinedSubclassPersister;
40
use Doctrine\ORM\Persisters\Entity\SingleTablePersister;
41
use Doctrine\ORM\Utility\NormalizeIdentifier;
42
use InvalidArgumentException;
43
use ProxyManager\Proxy\GhostObjectInterface;
44
use UnexpectedValueException;
45
46
/**
47
 * The UnitOfWork is responsible for tracking changes to objects during an
48
 * "object-level" transaction and for writing out changes to the database
49
 * in the correct order.
50
 *
51
 * Internal note: This class contains highly performance-sensitive code.
52
 */
53
class UnitOfWork implements PropertyChangedListener
54
{
55
    /**
56
     * An entity is in MANAGED state when its persistence is managed by an EntityManager.
57
     */
58
    public const STATE_MANAGED = 1;
59
60
    /**
61
     * An entity is new if it has just been instantiated (i.e. using the "new" operator)
62
     * and is not (yet) managed by an EntityManager.
63
     */
64
    public const STATE_NEW = 2;
65
66
    /**
67
     * A detached entity is an instance with persistent state and identity that is not
68
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
69
     */
70
    public const STATE_DETACHED = 3;
71
72
    /**
73
     * A removed entity instance is an instance with a persistent identity,
74
     * associated with an EntityManager, whose persistent state will be deleted
75
     * on commit.
76
     */
77
    public const STATE_REMOVED = 4;
78
79
    /**
80
     * Hint used to collect all primary keys of associated entities during hydration
81
     * and execute it in a dedicated query afterwards
82
     * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql
83
     */
84
    public const HINT_DEFEREAGERLOAD = 'deferEagerLoad';
85
86
    /**
87
     * The identity map that holds references to all managed entities that have
88
     * an identity. The entities are grouped by their class name.
89
     * Since all classes in a hierarchy must share the same identifier set,
90
     * we always take the root class name of the hierarchy.
91
     *
92
     * @var object[]
93
     */
94
    private $identityMap = [];
95
96
    /**
97
     * Map of all identifiers of managed entities.
98
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
99
     * Values are maps of entity identifiers, where its key is the column name and the value is the raw value.
100
     *
101
     * @var mixed[][]
102
     */
103
    private $entityIdentifiers = [];
104
105
    /**
106
     * Map of the original entity data of managed entities.
107
     * This is a 2-dimensional data structure (map of maps). Keys are object ids (spl_object_id).
108
     * Values are maps of entity data, where its key is the field name and the value is the converted
109
     * (convertToPHPValue) value.
110
     * This structure is used for calculating changesets at commit time.
111
     *
112
     * Internal: Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
113
     *           A value will only really be copied if the value in the entity is modified by the user.
114
     *
115
     * @var mixed[][]
116
     */
117
    private $originalEntityData = [];
118
119
    /**
120
     * Map of entity changes. Keys are object ids (spl_object_id).
121
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
122
     *
123
     * @var mixed[][]
124
     */
125
    private $entityChangeSets = [];
126
127
    /**
128
     * The (cached) states of any known entities.
129
     * Keys are object ids (spl_object_id).
130
     *
131
     * @var int[]
132
     */
133
    private $entityStates = [];
134
135
    /**
136
     * Map of entities that are scheduled for dirty checking at commit time.
137
     * This is only used for entities with a change tracking policy of DEFERRED_EXPLICIT.
138
     * Keys are object ids (spl_object_id).
139
     *
140
     * @var object[]
141
     */
142
    private $scheduledForSynchronization = [];
143
144
    /**
145
     * A list of all pending entity insertions.
146
     *
147
     * @var object[]
148
     */
149
    private $entityInsertions = [];
150
151
    /**
152
     * A list of all pending entity updates.
153
     *
154
     * @var object[]
155
     */
156
    private $entityUpdates = [];
157
158
    /**
159
     * Any pending extra updates that have been scheduled by persisters.
160
     *
161
     * @var object[]
162
     */
163
    private $extraUpdates = [];
164
165
    /**
166
     * A list of all pending entity deletions.
167
     *
168
     * @var object[]
169
     */
170
    private $entityDeletions = [];
171
172
    /**
173
     * New entities that were discovered through relationships that were not
174
     * marked as cascade-persist. During flush, this array is populated and
175
     * then pruned of any entities that were discovered through a valid
176
     * cascade-persist path. (Leftovers cause an error.)
177
     *
178
     * Keys are OIDs, payload is a two-item array describing the association
179
     * and the entity.
180
     *
181
     * @var object[][]|array[][] indexed by respective object spl_object_id()
182
     */
183
    private $nonCascadedNewDetectedEntities = [];
184
185
    /**
186
     * All pending collection deletions.
187
     *
188
     * @var Collection[]|object[][]
189
     */
190
    private $collectionDeletions = [];
191
192
    /**
193
     * All pending collection updates.
194
     *
195
     * @var Collection[]|object[][]
196
     */
197
    private $collectionUpdates = [];
198
199
    /**
200
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
201
     * At the end of the UnitOfWork all these collections will make new snapshots
202
     * of their data.
203
     *
204
     * @var Collection[]|object[][]
205
     */
206
    private $visitedCollections = [];
207
208
    /**
209
     * The EntityManager that "owns" this UnitOfWork instance.
210
     *
211
     * @var EntityManagerInterface
212
     */
213
    private $em;
214
215
    /**
216
     * The entity persister instances used to persist entity instances.
217
     *
218
     * @var EntityPersister[]
219
     */
220
    private $entityPersisters = [];
221
222
    /**
223
     * The collection persister instances used to persist collections.
224
     *
225
     * @var CollectionPersister[]
226
     */
227
    private $collectionPersisters = [];
228
229
    /**
230
     * The EventManager used for dispatching events.
231
     *
232
     * @var \Doctrine\Common\EventManager
233
     */
234
    private $eventManager;
235
236
    /**
237
     * The ListenersInvoker used for dispatching events.
238
     *
239
     * @var \Doctrine\ORM\Event\ListenersInvoker
240
     */
241
    private $listenersInvoker;
242
243
    /**
244
     * @var Instantiator
245
     */
246
    private $instantiator;
247
248
    /**
249
     * Orphaned entities that are scheduled for removal.
250
     *
251
     * @var object[]
252
     */
253
    private $orphanRemovals = [];
254
255
    /**
256
     * Read-Only objects are never evaluated
257
     *
258
     * @var object[]
259
     */
260
    private $readOnlyObjects = [];
261
262
    /**
263
     * Map of Entity Class-Names and corresponding IDs that should eager loaded when requested.
264
     *
265
     * @var mixed[][][]
266
     */
267
    private $eagerLoadingEntities = [];
268
269
    /**
270
     * @var bool
271
     */
272
    protected $hasCache = false;
273
274
    /**
275
     * Helper for handling completion of hydration
276
     *
277
     * @var HydrationCompleteHandler
278
     */
279
    private $hydrationCompleteHandler;
280
281
    /**
282
     * @var NormalizeIdentifier
283
     */
284
    private $normalizeIdentifier;
285
286
    /**
287
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
288
     */
289 2273
    public function __construct(EntityManagerInterface $em)
290
    {
291 2273
        $this->em                       = $em;
292 2273
        $this->eventManager             = $em->getEventManager();
293 2273
        $this->listenersInvoker         = new ListenersInvoker($em);
294 2273
        $this->hasCache                 = $em->getConfiguration()->isSecondLevelCacheEnabled();
295 2273
        $this->instantiator             = new Instantiator();
296 2273
        $this->hydrationCompleteHandler = new HydrationCompleteHandler($this->listenersInvoker, $em);
297 2273
        $this->normalizeIdentifier      = new NormalizeIdentifier();
298 2273
    }
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 1016
    public function commit()
316
    {
317
        // Raise preFlush
318 1016
        if ($this->eventManager->hasListeners(Events::preFlush)) {
319 2
            $this->eventManager->dispatchEvent(Events::preFlush, new PreFlushEventArgs($this->em));
320
        }
321
322 1016
        $this->computeChangeSets();
323
324 1014
        if (! ($this->entityInsertions ||
325 154
                $this->entityDeletions ||
326 119
                $this->entityUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array<mixed,object|mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
327 36
                $this->collectionUpdates ||
328 33
                $this->collectionDeletions ||
329 1014
                $this->orphanRemovals)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array<mixed,object|mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
330 21
            $this->dispatchOnFlushEvent();
331 21
            $this->dispatchPostFlushEvent();
332
333 21
            return; // Nothing to do.
334
        }
335
336 1009
        $this->assertThatThereAreNoUnintentionallyNonPersistedAssociations();
337
338 1007
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array<mixed,object|mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
339 15
            foreach ($this->orphanRemovals as $orphan) {
340 15
                $this->remove($orphan);
341
            }
342
        }
343
344 1007
        $this->dispatchOnFlushEvent();
345
346
        // Now we need a commit order to maintain referential integrity
347 1007
        $commitOrder = $this->getCommitOrder();
348
349 1007
        $conn = $this->em->getConnection();
350 1007
        $conn->beginTransaction();
351
352
        try {
353
            // Collection deletions (deletions of complete collections)
354 1007
            foreach ($this->collectionDeletions as $collectionToDelete) {
355 19
                $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
356
            }
357
358 1007
            if ($this->entityInsertions) {
359 1003
                foreach ($commitOrder as $class) {
360 1003
                    $this->executeInserts($class);
361
                }
362
            }
363
364 1006
            if ($this->entityUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->entityUpdates of type array<mixed,object|mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
365 108
                foreach ($commitOrder as $class) {
366 108
                    $this->executeUpdates($class);
367
                }
368
            }
369
370
            // Extra updates that were requested by persisters.
371 1002
            if ($this->extraUpdates) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->extraUpdates of type array<mixed,object> is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
372 33
                $this->executeExtraUpdates();
373
            }
374
375
            // Collection updates (deleteRows, updateRows, insertRows)
376 1002
            foreach ($this->collectionUpdates as $collectionToUpdate) {
377 528
                $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
378
            }
379
380
            // Entity deletions come last and need to be in reverse commit order
381 1002
            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 1002
            $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 1002
        $this->afterTransactionComplete();
402
403
        // Take new snapshots from visited collections
404 1002
        foreach ($this->visitedCollections as $coll) {
405 527
            $coll->takeSnapshot();
406
        }
407
408 1002
        $this->dispatchPostFlushEvent();
409
410
        // Clean up
411 1001
        $this->entityInsertions            =
412 1001
        $this->entityUpdates               =
413 1001
        $this->entityDeletions             =
414 1001
        $this->extraUpdates                =
415 1001
        $this->entityChangeSets            =
416 1001
        $this->collectionUpdates           =
417 1001
        $this->collectionDeletions         =
418 1001
        $this->visitedCollections          =
419 1001
        $this->scheduledForSynchronization =
420 1001
        $this->orphanRemovals              = [];
421 1001
    }
422
423
    /**
424
     * Computes the changesets of all entities scheduled for insertion.
425
     */
426 1016
    private function computeScheduleInsertsChangeSets()
427
    {
428 1016
        foreach ($this->entityInsertions as $entity) {
429 1007
            $class = $this->em->getClassMetadata(get_class($entity));
430
431 1007
            $this->computeChangeSet($class, $entity);
432
        }
433 1014
    }
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: ';
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 1002
    public function & getEntityChangeSet($entity)
462
    {
463 1002
        $oid  = spl_object_id($entity);
464 1002
        $data = [];
465
466 1002
        if (! isset($this->entityChangeSets[$oid])) {
467 2
            return $data;
468
        }
469
470 1002
        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 1017
    public function computeChangeSet(ClassMetadata $class, $entity)
507
    {
508 1017
        $oid = spl_object_id($entity);
509
510 1017
        if (isset($this->readOnlyObjects[$oid])) {
511 2
            return;
512
        }
513
514 1017
        if ($class->inheritanceType !== InheritanceType::NONE) {
515 330
            $class = $this->em->getClassMetadata(get_class($entity));
516
        }
517
518 1017
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER;
519
520 1017
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
521 135
            $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke);
522
        }
523
524 1017
        $actualData = [];
525
526 1017
        foreach ($class->getDeclaredPropertiesIterator() as $name => $property) {
527 1017
            $value = $property->getValue($entity);
528
529 1017
            if ($property instanceof ToManyAssociationMetadata && $value !== null) {
530 778
                if ($value instanceof PersistentCollection && $value->getOwner() === $entity) {
531 184
                    continue;
532
                }
533
534 775
                $value = $property->wrap($entity, $value, $this->em);
535
536 775
                $property->setValue($entity, $value);
537
538 775
                $actualData[$name] = $value;
539
540 775
                continue;
541
            }
542
543 1017
            if (( ! $class->isIdentifier($name)
544 1017
                    || ! $class->getProperty($name) instanceof FieldMetadata
545 1017
                    || ! $class->getProperty($name)->hasValueGenerator()
546 1017
                    || $class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
547 1017
                ) && (! $class->isVersioned() || $name !== $class->versionProperty->getName())) {
548 1017
                $actualData[$name] = $value;
549
            }
550
        }
551
552 1017
        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 1013
            $this->originalEntityData[$oid] = $actualData;
556 1013
            $changeSet                      = [];
557
558 1013
            foreach ($actualData as $propName => $actualValue) {
559 993
                $property = $class->getProperty($propName);
560
561 993
                if (($property instanceof FieldMetadata) ||
562 993
                    ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
563 993
                    $changeSet[$propName] = [null, $actualValue];
564
                }
565
            }
566
567 1013
            $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 1017
        foreach ($class->getDeclaredPropertiesIterator() as $property) {
662 1017
            if (! $property instanceof AssociationMetadata) {
663 1017
                continue;
664
            }
665
666 889
            $value = $property->getValue($entity);
667
668 889
            if ($value === null) {
669 628
                continue;
670
            }
671
672 865
            $this->computeAssociationChanges($property, $value);
673
674 857
            if ($property instanceof ManyToManyAssociationMetadata &&
675 857
                $value instanceof PersistentCollection &&
676 857
                ! isset($this->entityChangeSets[$oid]) &&
677 857
                $property->isOwningSide() &&
678 857
                $value->isDirty()) {
679 31
                $this->entityChangeSets[$oid]   = [];
680 31
                $this->originalEntityData[$oid] = $actualData;
681 857
                $this->entityUpdates[$oid]      = $entity;
682
            }
683
        }
684 1009
    }
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 1016
    public function computeChangeSets()
692
    {
693
        // Compute changes for INSERTed entities first. This must always happen.
694 1016
        $this->computeScheduleInsertsChangeSets();
695
696
        // Compute changes for other MANAGED entities. Change tracking policies take effect here.
697 1014
        foreach ($this->identityMap as $className => $entities) {
698 447
            $class = $this->em->getClassMetadata($className);
699
700
            // Skip class if instances are read-only
701 447
            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 446
                case ($class->changeTrackingPolicy === ChangeTrackingPolicy::DEFERRED_IMPLICIT):
709 444
                    $entitiesToProcess = $entities;
710 444
                    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 446
            foreach ($entitiesToProcess as $entity) {
721
                // Ignore uninitialized proxy objects
722 426
                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 424
                $oid = spl_object_id($entity);
728
729 424
                if (! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) {
730 446
                    $this->computeChangeSet($class, $entity);
731
                }
732
            }
733
        }
734 1014
    }
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 865
    private function computeAssociationChanges(AssociationMetadata $association, $value)
746
    {
747 865
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
748 30
            return;
749
        }
750
751 864
        if ($value instanceof PersistentCollection && $value->isDirty()) {
752 531
            $coid = spl_object_id($value);
753
754 531
            $this->collectionUpdates[$coid]  = $value;
755 531
            $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")
760
        // Unwrap. Uninitialized collections will simply be empty.
761 864
        $unwrappedValue = ($association instanceof ToOneAssociationMetadata) ? [$value] : $value->unwrap();
762 864
        $targetEntity   = $association->getTargetEntity();
763 864
        $targetClass    = $this->em->getClassMetadata($targetEntity);
764
765 864
        foreach ($unwrappedValue as $key => $entry) {
766 721
            if (! ($entry instanceof $targetEntity)) {
767 8
                throw ORMInvalidArgumentException::invalidAssociation($targetClass, $association, $entry);
768
            }
769
770 713
            $state = $this->getEntityState($entry, self::STATE_NEW);
771
772 713
            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 713
                case self::STATE_NEW:
783 41
                    if (! in_array('persist', $association->getCascade())) {
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 706
                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 706
                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 713
                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 856
    }
814
815
    /**
816
     * @param \Doctrine\ORM\Mapping\ClassMetadata $class
817
     * @param object                              $entity
818
     */
819 1028
    private function persistNew($class, $entity)
820
    {
821 1028
        $oid    = spl_object_id($entity);
822 1028
        $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::prePersist);
823
824 1028
        if ($invoke !== ListenersInvoker::INVOKE_NONE) {
825 137
            $this->listenersInvoker->invoke($class, Events::prePersist, $entity, new LifecycleEventArgs($entity, $this->em), $invoke);
826
        }
827
828 1028
        $generationPlan = $class->getValueGenerationPlan();
829 1028
        $persister      = $this->getEntityPersister($class->getClassName());
830 1028
        $generationPlan->executeImmediate($this->em, $entity);
831
832 1028
        if (! $generationPlan->containsDeferred()) {
833 269
            $id                            = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
834 269
            $this->entityIdentifiers[$oid] = $id;
835
        }
836
837 1028
        $this->entityStates[$oid] = self::STATE_MANAGED;
838
839 1028
        $this->scheduleForInsert($entity);
840 1028
    }
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 1003
    private function executeInserts(ClassMetadata $class) : void
929
    {
930 1003
        $className      = $class->getClassName();
931 1003
        $persister      = $this->getEntityPersister($className);
932 1003
        $invoke         = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
933 1003
        $generationPlan = $class->getValueGenerationPlan();
934
935 1003
        foreach ($this->entityInsertions as $oid => $entity) {
936 1003
            if ($this->em->getClassMetadata(get_class($entity))->getClassName() !== $className) {
937 857
                continue;
938
            }
939
940 1003
            $persister->insert($entity);
941
942 1002
            if ($generationPlan->containsDeferred()) {
943
                // Entity has post-insert IDs
944 910
                $oid = spl_object_id($entity);
945 910
                $id  = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $persister->getIdentifier($entity));
946
947 910
                $this->entityIdentifiers[$oid]  = $id;
948 910
                $this->entityStates[$oid]       = self::STATE_MANAGED;
949 910
                $this->originalEntityData[$oid] = $id + $this->originalEntityData[$oid];
950
951 910
                $this->addToIdentityMap($entity);
952
            }
953
954 1002
            unset($this->entityInsertions[$oid]);
955
956 1002
            if ($invoke !== ListenersInvoker::INVOKE_NONE) {
957 133
                $eventArgs = new LifecycleEventArgs($entity, $this->em);
958
959 1002
                $this->listenersInvoker->invoke($class, Events::postPersist, $entity, $eventArgs, $invoke);
960
            }
961
        }
962 1003
    }
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: ';
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;
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 1007
    private function getCommitOrder()
1052
    {
1053 1007
        $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 1007
        $newNodes = [];
1061
1062 1007
        foreach (\array_merge($this->entityInsertions, $this->entityUpdates, $this->entityDeletions) as $entity) {
1063 1007
            $class = $this->em->getClassMetadata(get_class($entity));
1064
1065 1007
            if ($calc->hasNode($class->getClassName())) {
1066 636
                continue;
1067
            }
1068
1069 1007
            $calc->addNode($class->getClassName(), $class);
1070
1071 1007
            $newNodes[] = $class;
1072
        }
1073
1074
        // Calculate dependencies for new nodes
1075 1007
        while ($class = array_pop($newNodes)) {
1076 1007
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
1077 1007
                if (! ($property instanceof ToOneAssociationMetadata && $property->isOwningSide())) {
1078 1007
                    continue;
1079
                }
1080
1081 837
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1082
1083 837
                if (! $calc->hasNode($targetClass->getClassName())) {
1084 645
                    $calc->addNode($targetClass->getClassName(), $targetClass);
1085
1086 645
                    $newNodes[] = $targetClass;
1087
                }
1088
1089 837
                $weight = ! array_filter(
1090 837
                    $property->getJoinColumns(),
1091 837
                    function (JoinColumnMetadata $joinColumn) {
1092 837
                        return $joinColumn->isNullable();
1093 837
                    }
1094
                );
1095
1096 837
                $calc->addDependency($targetClass->getClassName(), $class->getClassName(), $weight);
1097
1098
                // If the target class has mapped subclasses, these share the same dependency.
1099 837
                if (! $targetClass->getSubClasses()) {
1100 832
                    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 1007
        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 1029
    public function scheduleForInsert($entity)
1130
    {
1131 1029
        $oid = spl_object_id($entity);
1132
1133 1029
        if (isset($this->entityUpdates[$oid])) {
1134
            throw new InvalidArgumentException('Dirty entity can not be scheduled for insertion.');
1135
        }
1136
1137 1029
        if (isset($this->entityDeletions[$oid])) {
1138 1
            throw ORMInvalidArgumentException::scheduleInsertForRemovedEntity($entity);
1139
        }
1140 1029
        if (isset($this->originalEntityData[$oid]) && ! isset($this->entityInsertions[$oid])) {
1141 1
            throw ORMInvalidArgumentException::scheduleInsertForManagedEntity($entity);
1142
        }
1143
1144 1029
        if (isset($this->entityInsertions[$oid])) {
1145 1
            throw ORMInvalidArgumentException::scheduleInsertTwice($entity);
1146
        }
1147
1148 1029
        $this->entityInsertions[$oid] = $entity;
1149
1150 1029
        if (isset($this->entityIdentifiers[$oid])) {
1151 269
            $this->addToIdentityMap($entity);
1152
        }
1153
1154 1029
        if ($entity instanceof NotifyPropertyChanged) {
1155 5
            $entity->addPropertyChangedListener($this);
1156
        }
1157 1029
    }
1158
1159
    /**
1160
     * Checks whether an entity is scheduled for insertion.
1161
     *
1162
     * @param object $entity
1163
     *
1164
     * @return bool
1165
     */
1166 624
    public function isScheduledForInsert($entity)
1167
    {
1168 624
        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 1097
    public function addToIdentityMap($entity)
1325
    {
1326 1097
        $classMetadata = $this->em->getClassMetadata(get_class($entity));
1327 1097
        $identifier    = $this->entityIdentifiers[spl_object_id($entity)];
1328
1329 1097
        if (empty($identifier) || in_array(null, $identifier, true)) {
1330 6
            throw ORMInvalidArgumentException::entityWithoutIdentity($classMetadata->getClassName(), $entity);
1331
        }
1332
1333 1091
        $idHash    = implode(' ', $identifier);
1334 1091
        $className = $classMetadata->getRootClassName();
1335
1336 1091
        if (isset($this->identityMap[$className][$idHash])) {
1337 32
            return false;
1338
        }
1339
1340 1091
        $this->identityMap[$className][$idHash] = $entity;
1341
1342 1091
        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 1037
    public function getEntityState($entity, $assume = null)
1357
    {
1358 1037
        $oid = spl_object_id($entity);
1359
1360 1037
        if (isset($this->entityStates[$oid])) {
1361 753
            return $this->entityStates[$oid];
1362
        }
1363
1364 1032
        if ($assume !== null) {
1365 1029
            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;
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 1029
    public function persist($entity)
1543
    {
1544 1029
        $visited = [];
1545
1546 1029
        $this->doPersist($entity, $visited);
1547 1022
    }
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 1029
    private function doPersist($entity, array &$visited)
1562
    {
1563 1029
        $oid = spl_object_id($entity);
1564
1565 1029
        if (isset($visited[$oid])) {
1566 109
            return; // Prevent infinite recursion
1567
        }
1568
1569 1029
        $visited[$oid] = $entity; // Mark visited
1570
1571 1029
        $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 1029
        $entityState = $this->getEntityState($entity, self::STATE_NEW);
1578
1579
        switch ($entityState) {
1580 1029
            case self::STATE_MANAGED:
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 1029
            case self::STATE_NEW:
1588 1028
                $this->persistNew($class, $entity);
1589 1028
                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 1029
        $this->cascadePersist($entity, $visited);
1610 1022
    }
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()))) {
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 1029
    private function cascadePersist($entity, array &$visited)
1774
    {
1775 1029
        $class = $this->em->getClassMetadata(get_class($entity));
1776
1777 1029
        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 1029
        foreach ($class->getDeclaredPropertiesIterator() as $association) {
1783 1029
            if (! ($association instanceof AssociationMetadata && in_array('persist', $association->getCascade()))) {
1784 1029
                continue;
1785
            }
1786
1787
            /** @var AssociationMetadata $association */
1788 651
            $relatedEntities = $association->getValue($entity);
1789 651
            $targetEntity    = $association->getTargetEntity();
1790
1791
            switch (true) {
1792 651
                case ($relatedEntities instanceof PersistentCollection):
1793
                    // Unwrap so that foreach() does not initialize
1794 13
                    $relatedEntities = $relatedEntities->unwrap();
1795
                    // break; is commented intentionally!
1796
1797 651
                case ($relatedEntities instanceof Collection):
1798 589
                case (is_array($relatedEntities)):
1799 547
                    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 544
                    foreach ($relatedEntities as $relatedEntity) {
1808 285
                        $this->doPersist($relatedEntity, $visited);
1809
                    }
1810
1811 544
                    break;
1812
1813 579
                case ($relatedEntities !== null):
1814 247
                    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 243
                    $this->doPersist($relatedEntities, $visited);
1823 243
                    break;
1824
1825 645
                default:
1826
                    // Do nothing
1827
            }
1828
        }
1829 1022
    }
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()))) {
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 1213
    public function clear()
1946
    {
1947 1213
        $this->entityPersisters               =
1948 1213
        $this->collectionPersisters           =
1949 1213
        $this->eagerLoadingEntities           =
1950 1213
        $this->identityMap                    =
1951 1213
        $this->entityIdentifiers              =
1952 1213
        $this->originalEntityData             =
1953 1213
        $this->entityChangeSets               =
1954 1213
        $this->entityStates                   =
1955 1213
        $this->scheduledForSynchronization    =
1956 1213
        $this->entityInsertions               =
1957 1213
        $this->entityUpdates                  =
1958 1213
        $this->entityDeletions                =
1959 1213
        $this->collectionDeletions            =
1960 1213
        $this->collectionUpdates              =
1961 1213
        $this->extraUpdates                   =
1962 1213
        $this->readOnlyObjects                =
1963 1213
        $this->visitedCollections             =
1964 1213
        $this->nonCascadedNewDetectedEntities =
1965 1213
        $this->orphanRemovals                 = [];
1966 1213
    }
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 669
    public function newInstance(ClassMetadata $class)
2029
    {
2030 669
        $entity = $this->instantiator->instantiate($class->getClassName());
2031
2032 669
        if ($entity instanceof EntityManagerAware) {
2033 5
            $entity->injectEntityManager($this->em, $class);
2034
        }
2035
2036 669
        return $entity;
2037
    }
2038
2039
    /**
2040
     * INTERNAL:
2041
     * Creates an entity. Used for reconstitution of persistent entities.
2042
     *
2043
     * Internal note: Highly performance-sensitive method.
2044
     *
2045
     * @ignore
2046
     *
2047
     * @param string  $className The name of the entity class.
2048
     * @param mixed[] $data      The data for the entity.
2049
     * @param mixed[] $hints     Any hints to account for during reconstitution/lookup of the entity.
2050
     *
2051
     * @return object The managed entity instance.
2052
     *
2053
     * @todo Rename: getOrCreateEntity
2054
     */
2055 807
    public function createEntity($className, array $data, &$hints = [])
2056
    {
2057 807
        $class  = $this->em->getClassMetadata($className);
2058 807
        $id     = $this->em->getIdentifierFlattener()->flattenIdentifier($class, $data);
2059 807
        $idHash = implode(' ', $id);
2060
2061 807
        if (isset($this->identityMap[$class->getRootClassName()][$idHash])) {
2062 307
            $entity = $this->identityMap[$class->getRootClassName()][$idHash];
2063 307
            $oid    = spl_object_id($entity);
2064
2065 307
            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 307
            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 293
                if (! isset($hints[Query::HINT_REFRESH])
2085 293
                    || (isset($hints[Query::HINT_REFRESH_ENTITY]) && $hints[Query::HINT_REFRESH_ENTITY] !== $entity)) {
2086 230
                    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 666
            $entity = $this->newInstance($class);
2098 666
            $oid    = spl_object_id($entity);
2099
2100 666
            $this->entityIdentifiers[$oid]  = $id;
2101 666
            $this->entityStates[$oid]       = self::STATE_MANAGED;
2102 666
            $this->originalEntityData[$oid] = $data;
2103
2104 666
            $this->identityMap[$class->getRootClassName()][$idHash] = $entity;
2105
        }
2106
2107 699
        if ($entity instanceof NotifyPropertyChanged) {
2108 3
            $entity->addPropertyChangedListener($this);
2109
        }
2110
2111 699
        foreach ($data as $field => $value) {
2112 699
            $property = $class->getProperty($field);
2113
2114 699
            if ($property instanceof FieldMetadata) {
2115 699
                $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 699
        unset($this->eagerLoadingEntities[$class->getRootClassName()][$idHash]);
2121
2122 699
        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 699
        if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD])) {
2128 34
            return $entity;
2129
        }
2130
2131 665
        foreach ($class->getDeclaredPropertiesIterator() as $field => $association) {
2132 665
            if (! ($association instanceof AssociationMetadata)) {
2133 665
                continue;
2134
            }
2135
2136
            // Check if the association is not among the fetch-joined associations already.
2137 574
            if (isset($hints['fetchAlias'], $hints['fetched'][$hints['fetchAlias']][$field])) {
2138 247
                continue;
2139
            }
2140
2141 551
            $targetEntity = $association->getTargetEntity();
2142 551
            $targetClass  = $this->em->getClassMetadata($targetEntity);
2143
2144 551
            if ($association instanceof ToManyAssociationMetadata) {
2145
                // Ignore if its a cached collection
2146 471
                if (isset($hints[Query::HINT_CACHE_ENABLED]) &&
2147 471
                    $association->getValue($entity) instanceof PersistentCollection) {
2148
                    continue;
2149
                }
2150
2151 471
                $hasDataField = isset($data[$field]);
2152
2153
                // use the given collection
2154 471
                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 471
                $pColl = $association->wrap($entity, $hasDataField ? $data[$field] : [], $this->em);
2166
2167 471
                $pColl->setInitialized($hasDataField);
2168
2169 471
                $association->setValue($entity, $pColl);
2170
2171 471
                if ($association->getFetchMode() === FetchMode::EAGER) {
2172 4
                    $this->loadCollection($pColl);
2173 4
                    $pColl->takeSnapshot();
2174
                }
2175
2176 471
                $this->originalEntityData[$oid][$field] = $pColl;
2177
2178 471
                continue;
2179
            }
2180
2181 477
            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 477
            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 470
            $associatedId = [];
2213
2214
            // TODO: Is this even computed right in all cases of composite keys?
2215 470
            foreach ($association->getJoinColumns() as $joinColumn) {
2216
                /** @var JoinColumnMetadata $joinColumn */
2217 470
                $joinColumnName  = $joinColumn->getColumnName();
2218 470
                $joinColumnValue = $data[$joinColumnName] ?? null;
2219 470
                $targetField     = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
2220
2221 470
                if ($joinColumnValue === null && in_array($targetField, $targetClass->identifier, true)) {
2222
                    // the missing key is part of target's entity primary key
2223 275
                    $associatedId = [];
2224
2225 275
                    continue;
2226
                }
2227
2228 284
                $associatedId[$targetField] = $joinColumnValue;
2229
            }
2230
2231 470
            if (! $associatedId) {
2232
                // Foreign key is NULL
2233 275
                $association->setValue($entity, null);
2234 275
                $this->originalEntityData[$oid][$field] = null;
2235
2236 275
                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])):
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()):
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 665
        $this->hydrationCompleteHandler->deferPostLoadInvoking($class, $entity);
2334
2335 665
        return $entity;
2336
    }
2337
2338 868
    public function triggerEagerLoads()
2339
    {
2340 868
        if (! $this->eagerLoadingEntities) {
2341 868
            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 305
    public function setOriginalEntityProperty($oid, $property, $value)
2429
    {
2430 305
        $this->originalEntityData[$oid][$property] = $value;
2431 305
    }
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 1086
    public function getEntityPersister($entityName)
2531
    {
2532 1086
        if (isset($this->entityPersisters[$entityName])) {
2533 1029
            return $this->entityPersisters[$entityName];
2534
        }
2535
2536 1086
        $class = $this->em->getClassMetadata($entityName);
2537
2538
        switch (true) {
2539 1086
            case ($class->inheritanceType === InheritanceType::NONE):
2540 1045
                $persister = new BasicEntityPersister($this->em, $class);
2541 1045
                break;
2542
2543 382
            case ($class->inheritanceType === InheritanceType::SINGLE_TABLE):
2544 223
                $persister = new SingleTablePersister($this->em, $class);
2545 223
                break;
2546
2547 352
            case ($class->inheritanceType === InheritanceType::JOINED):
2548 352
                $persister = new JoinedSubclassPersister($this->em, $class);
2549 352
                break;
2550
2551
            default:
2552
                throw new \RuntimeException('No persister found for entity.');
2553
        }
2554
2555 1086
        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 1086
        $this->entityPersisters[$entityName] = $persister;
2563
2564 1086
        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 567
    public function getCollectionPersister(ToManyAssociationMetadata $association)
2573
    {
2574 567
        $role = $association->getCache()
2575 78
            ? sprintf('%s::%s', $association->getSourceEntity(), $association->getName())
2576 567
            : get_class($association);
2577
2578 567
        if (isset($this->collectionPersisters[$role])) {
2579 434
            return $this->collectionPersisters[$role];
2580
        }
2581
2582 567
        $persister = $association instanceof OneToManyAssociationMetadata
2583 404
            ? new OneToManyPersister($this->em)
2584 567
            : new ManyToManyPersister($this->em);
2585
2586 567
        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 567
        $this->collectionPersisters[$role] = $persister;
2594
2595 567
        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 1002
        $this->performCallbackOnCachedPersister(function (CachedPersister $persister) {
2784 94
            $persister->afterTransactionComplete();
2785 1002
        });
2786 1002
    }
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 1007
    private function performCallbackOnCachedPersister(callable $callback)
2802
    {
2803 1007
        if (! $this->hasCache) {
2804 913
            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 1012
    private function dispatchOnFlushEvent()
2815
    {
2816 1012
        if ($this->eventManager->hasListeners(Events::onFlush)) {
2817 4
            $this->eventManager->dispatchEvent(Events::onFlush, new OnFlushEventArgs($this->em));
2818
        }
2819 1012
    }
2820
2821 1007
    private function dispatchPostFlushEvent()
2822
    {
2823 1007
        if ($this->eventManager->hasListeners(Events::postFlush)) {
2824 5
            $this->eventManager->dispatchEvent(Events::postFlush, new PostFlushEventArgs($this->em));
2825
        }
2826 1006
    }
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 1009
    private function assertThatThereAreNoUnintentionallyNonPersistedAssociations() : void
2866
    {
2867 1009
        $entitiesNeedingCascadePersist = \array_diff_key($this->nonCascadedNewDetectedEntities, $this->entityInsertions);
2868
2869 1009
        $this->nonCascadedNewDetectedEntities = [];
2870
2871 1009
        if ($entitiesNeedingCascadePersist) {
2872 4
            throw ORMInvalidArgumentException::newEntitiesFoundThroughRelationships(
2873 4
                \array_values($entitiesNeedingCascadePersist)
2874
            );
2875
        }
2876 1007
    }
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 883
    public function hydrationComplete()
2885
    {
2886 883
        $this->hydrationCompleteHandler->hydrationComplete();
2887 883
    }
2888
}
2889