Completed
Push — master ( 09b86b...ba0798 )
by Andreas
20:05 queued 12s
created

lib/Doctrine/ODM/MongoDB/UnitOfWork.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use const E_USER_DEPRECATED;
29
use function array_filter;
30
use function count;
31
use function get_class;
32
use function in_array;
33
use function is_array;
34
use function is_object;
35
use function method_exists;
36
use function preg_match;
37
use function serialize;
38
use function spl_object_hash;
39
use function sprintf;
40
use function trigger_error;
41
42
/**
43
 * The UnitOfWork is responsible for tracking changes to objects during an
44
 * "object-level" transaction and for writing out changes to the database
45
 * in the correct order.
46
 *
47
 * @final
48
 */
49
class UnitOfWork implements PropertyChangedListener
50
{
51
    /**
52
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
53
     */
54
    public const STATE_MANAGED = 1;
55
56
    /**
57
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
58
     * and is not (yet) managed by a DocumentManager.
59
     */
60
    public const STATE_NEW = 2;
61
62
    /**
63
     * A detached document is an instance with a persistent identity that is not
64
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
65
     */
66
    public const STATE_DETACHED = 3;
67
68
    /**
69
     * A removed document instance is an instance with a persistent identity,
70
     * associated with a DocumentManager, whose persistent state has been
71
     * deleted (or is scheduled for deletion).
72
     */
73
    public const STATE_REMOVED = 4;
74
75
    /**
76
     * The identity map holds references to all managed documents.
77
     *
78
     * Documents are grouped by their class name, and then indexed by the
79
     * serialized string of their database identifier field or, if the class
80
     * has no identifier, the SPL object hash. Serializing the identifier allows
81
     * differentiation of values that may be equal (via type juggling) but not
82
     * identical.
83
     *
84
     * Since all classes in a hierarchy must share the same identifier set,
85
     * we always take the root class name of the hierarchy.
86
     *
87
     * @var array
88
     */
89
    private $identityMap = [];
90
91
    /**
92
     * Map of all identifiers of managed documents.
93
     * Keys are object ids (spl_object_hash).
94
     *
95
     * @var array
96
     */
97
    private $documentIdentifiers = [];
98
99
    /**
100
     * Map of the original document data of managed documents.
101
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
102
     * at commit time.
103
     *
104
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
105
     *           A value will only really be copied if the value in the document is modified
106
     *           by the user.
107
     *
108
     * @var array
109
     */
110
    private $originalDocumentData = [];
111
112
    /**
113
     * Map of document changes. Keys are object ids (spl_object_hash).
114
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
115
     *
116
     * @var array
117
     */
118
    private $documentChangeSets = [];
119
120
    /**
121
     * The (cached) states of any known documents.
122
     * Keys are object ids (spl_object_hash).
123
     *
124
     * @var array
125
     */
126
    private $documentStates = [];
127
128
    /**
129
     * Map of documents that are scheduled for dirty checking at commit time.
130
     *
131
     * Documents are grouped by their class name, and then indexed by their SPL
132
     * object hash. This is only used for documents with a change tracking
133
     * policy of DEFERRED_EXPLICIT.
134
     *
135
     * @var array
136
     */
137
    private $scheduledForSynchronization = [];
138
139
    /**
140
     * A list of all pending document insertions.
141
     *
142
     * @var array
143
     */
144
    private $documentInsertions = [];
145
146
    /**
147
     * A list of all pending document updates.
148
     *
149
     * @var array
150
     */
151
    private $documentUpdates = [];
152
153
    /**
154
     * A list of all pending document upserts.
155
     *
156
     * @var array
157
     */
158
    private $documentUpserts = [];
159
160
    /**
161
     * A list of all pending document deletions.
162
     *
163
     * @var array
164
     */
165
    private $documentDeletions = [];
166
167
    /**
168
     * All pending collection deletions.
169
     *
170
     * @var array
171
     */
172
    private $collectionDeletions = [];
173
174
    /**
175
     * All pending collection updates.
176
     *
177
     * @var array
178
     */
179
    private $collectionUpdates = [];
180
181
    /**
182
     * A list of documents related to collections scheduled for update or deletion
183
     *
184
     * @var array
185
     */
186
    private $hasScheduledCollections = [];
187
188
    /**
189
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
190
     * At the end of the UnitOfWork all these collections will make new snapshots
191
     * of their data.
192
     *
193
     * @var array
194
     */
195
    private $visitedCollections = [];
196
197
    /**
198
     * The DocumentManager that "owns" this UnitOfWork instance.
199
     *
200
     * @var DocumentManager
201
     */
202
    private $dm;
203
204
    /**
205
     * The EventManager used for dispatching events.
206
     *
207
     * @var EventManager
208
     */
209
    private $evm;
210
211
    /**
212
     * Additional documents that are scheduled for removal.
213
     *
214
     * @var array
215
     */
216
    private $orphanRemovals = [];
217
218
    /**
219
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
220
     *
221
     * @var HydratorFactory
222
     */
223
    private $hydratorFactory;
224
225
    /**
226
     * The document persister instances used to persist document instances.
227
     *
228
     * @var array
229
     */
230
    private $persisters = [];
231
232
    /**
233
     * The collection persister instance used to persist changes to collections.
234
     *
235
     * @var Persisters\CollectionPersister
236
     */
237
    private $collectionPersister;
238
239
    /**
240
     * The persistence builder instance used in DocumentPersisters.
241
     *
242
     * @var PersistenceBuilder|null
243
     */
244
    private $persistenceBuilder;
245
246
    /**
247
     * Array of parent associations between embedded documents.
248
     *
249
     * @var array
250
     */
251
    private $parentAssociations = [];
252
253
    /** @var LifecycleEventManager */
254
    private $lifecycleEventManager;
255
256
    /**
257
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
258
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
259
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
260
     *
261
     * @var array
262
     */
263
    private $embeddedDocumentsRegistry = [];
264
265
    /** @var int */
266
    private $commitsInProgress = 0;
267
268
    /**
269
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
270
     */
271 1656
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
272
    {
273 1656
        if (self::class !== static::class) {
274
            @trigger_error(sprintf('The class "%s" extends "%s" which will be final in MongoDB ODM 2.0.', static::class, self::class), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
275
        }
276 1656
        $this->dm                    = $dm;
277 1656
        $this->evm                   = $evm;
278 1656
        $this->hydratorFactory       = $hydratorFactory;
279 1656
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
280 1656
    }
281
282
    /**
283
     * Factory for returning new PersistenceBuilder instances used for preparing data into
284
     * queries for insert persistence.
285
     *
286
     * @internal
287
     */
288 1135
    public function getPersistenceBuilder() : PersistenceBuilder
289
    {
290 1135
        if (! $this->persistenceBuilder) {
291 1135
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
292
        }
293 1135
        return $this->persistenceBuilder;
294
    }
295
296
    /**
297
     * Sets the parent association for a given embedded document.
298
     *
299
     * @internal
300
     */
301 209
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
302
    {
303 209
        $oid                                   = spl_object_hash($document);
304 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
305 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
306 209
    }
307
308
    /**
309
     * Gets the parent association for a given embedded document.
310
     *
311
     *     <code>
312
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
313
     *     </code>
314
     */
315 233
    public function getParentAssociation(object $document) : ?array
316
    {
317 233
        $oid = spl_object_hash($document);
318
319 233
        return $this->parentAssociations[$oid] ?? null;
320
    }
321
322
    /**
323
     * Get the document persister instance for the given document name
324
     */
325 1133
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
326
    {
327 1133
        if (! isset($this->persisters[$documentName])) {
328 1120
            $class                           = $this->dm->getClassMetadata($documentName);
329 1120
            $pb                              = $this->getPersistenceBuilder();
330 1120
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
331
        }
332 1133
        return $this->persisters[$documentName];
333
    }
334
335
    /**
336
     * Get the collection persister instance.
337
     */
338 1133
    public function getCollectionPersister() : CollectionPersister
339
    {
340 1133
        if (! isset($this->collectionPersister)) {
341 1133
            $pb                        = $this->getPersistenceBuilder();
342 1133
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
343
        }
344 1133
        return $this->collectionPersister;
345
    }
346
347
    /**
348
     * Set the document persister instance to use for the given document name
349
     *
350
     * @internal
351
     */
352 13
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
353
    {
354 13
        $this->persisters[$documentName] = $persister;
355 13
    }
356
357
    /**
358
     * Commits the UnitOfWork, executing all operations that have been postponed
359
     * up to this point. The state of all managed documents will be synchronized with
360
     * the database.
361
     *
362
     * The operations are executed in the following order:
363
     *
364
     * 1) All document insertions
365
     * 2) All document updates
366
     * 3) All document deletions
367
     *
368
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
369
     */
370 614
    public function commit(array $options = []) : void
371
    {
372
        // Raise preFlush
373 614
        if ($this->evm->hasListeners(Events::preFlush)) {
374
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
375
        }
376
377
        // Compute changes done since last commit.
378 614
        $this->computeChangeSets();
379
380 613
        if (! ($this->documentInsertions ||
381 257
            $this->documentUpserts ||
382 215
            $this->documentDeletions ||
383 198
            $this->documentUpdates ||
384 22
            $this->collectionUpdates ||
385 22
            $this->collectionDeletions ||
386 613
            $this->orphanRemovals)
387
        ) {
388 22
            return; // Nothing to do.
389
        }
390
391 610
        $this->commitsInProgress++;
392 610
        if ($this->commitsInProgress > 1) {
393
            throw MongoDBException::commitInProgress();
394
        }
395
        try {
396 610
            if ($this->orphanRemovals) {
397 55
                foreach ($this->orphanRemovals as $removal) {
398 55
                    $this->remove($removal);
399
                }
400
            }
401
402
            // Raise onFlush
403 610
            if ($this->evm->hasListeners(Events::onFlush)) {
404 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
405
            }
406
407 609
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
408 86
                [$class, $documents] = $classAndDocuments;
409 86
                $this->executeUpserts($class, $documents, $options);
410
            }
411
412 609
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
413 533
                [$class, $documents] = $classAndDocuments;
414 533
                $this->executeInserts($class, $documents, $options);
415
            }
416
417 596
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
418 236
                [$class, $documents] = $classAndDocuments;
419 236
                $this->executeUpdates($class, $documents, $options);
420
            }
421
422 596
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
423 79
                [$class, $documents] = $classAndDocuments;
424 79
                $this->executeDeletions($class, $documents, $options);
425
            }
426
427
            // Raise postFlush
428 596
            if ($this->evm->hasListeners(Events::postFlush)) {
429
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
430
            }
431
432
            // Clear up
433 596
            $this->documentInsertions          =
434 596
            $this->documentUpserts             =
435 596
            $this->documentUpdates             =
436 596
            $this->documentDeletions           =
437 596
            $this->documentChangeSets          =
438 596
            $this->collectionUpdates           =
439 596
            $this->collectionDeletions         =
440 596
            $this->visitedCollections          =
441 596
            $this->scheduledForSynchronization =
442 596
            $this->orphanRemovals              =
443 596
            $this->hasScheduledCollections     = [];
444 596
        } finally {
445 610
            $this->commitsInProgress--;
446
        }
447 596
    }
448
449
    /**
450
     * Groups a list of scheduled documents by their class.
451
     */
452 609
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
453
    {
454 609
        if (empty($documents)) {
455 609
            return [];
456
        }
457 608
        $divided = [];
458 608
        $embeds  = [];
459 608
        foreach ($documents as $oid => $d) {
460 608
            $className = get_class($d);
461 608
            if (isset($embeds[$className])) {
462 81
                continue;
463
            }
464 608
            if (isset($divided[$className])) {
465 166
                $divided[$className][1][$oid] = $d;
466 166
                continue;
467
            }
468 608
            $class = $this->dm->getClassMetadata($className);
469 608
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
470 183
                $embeds[$className] = true;
471 183
                continue;
472
            }
473 608
            if (empty($divided[$class->name])) {
474 608
                $divided[$class->name] = [$class, [$oid => $d]];
475
            } else {
476 4
                $divided[$class->name][1][$oid] = $d;
477
            }
478
        }
479 608
        return $divided;
480
    }
481
482
    /**
483
     * Compute changesets of all documents scheduled for insertion.
484
     *
485
     * Embedded documents will not be processed.
486
     */
487 619
    private function computeScheduleInsertsChangeSets() : void
488
    {
489 619
        foreach ($this->documentInsertions as $document) {
490 546
            $class = $this->dm->getClassMetadata(get_class($document));
491 546
            if ($class->isEmbeddedDocument) {
492 165
                continue;
493
            }
494
495 540
            $this->computeChangeSet($class, $document);
496
        }
497 618
    }
498
499
    /**
500
     * Compute changesets of all documents scheduled for upsert.
501
     *
502
     * Embedded documents will not be processed.
503
     */
504 618
    private function computeScheduleUpsertsChangeSets() : void
505
    {
506 618
        foreach ($this->documentUpserts as $document) {
507 85
            $class = $this->dm->getClassMetadata(get_class($document));
508 85
            if ($class->isEmbeddedDocument) {
509
                continue;
510
            }
511
512 85
            $this->computeChangeSet($class, $document);
513
        }
514 618
    }
515
516
    /**
517
     * Gets the changeset for a document.
518
     *
519
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
520
     */
521 612
    public function getDocumentChangeSet(object $document) : array
522
    {
523 612
        $oid = spl_object_hash($document);
524
525 612
        return $this->documentChangeSets[$oid] ?? [];
526
    }
527
528
    /**
529
     * Sets the changeset for a document.
530
     *
531
     * @internal
532
     */
533 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
534
    {
535 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
536 1
    }
537
538
    /**
539
     * Get a documents actual data, flattening all the objects to arrays.
540
     *
541
     * @internal
542
     *
543
     * @return array
544
     */
545 619
    public function getDocumentActualData(object $document) : array
546
    {
547 619
        $class      = $this->dm->getClassMetadata(get_class($document));
548 619
        $actualData = [];
549 619
        foreach ($class->reflFields as $name => $refProp) {
550 619
            $mapping = $class->fieldMappings[$name];
551
            // skip not saved fields
552 619
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
553 199
                continue;
554
            }
555 619
            $value = $refProp->getValue($document);
556 619
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
557 619
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
558
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
559 402
                if (! $value instanceof Collection) {
560 147
                    $value = new ArrayCollection($value);
561
                }
562
563
                // Inject PersistentCollection
564 402
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
565 402
                $coll->setOwner($document, $mapping);
566 402
                $coll->setDirty(! $value->isEmpty());
567 402
                $class->reflFields[$name]->setValue($document, $coll);
568 402
                $actualData[$name] = $coll;
569
            } else {
570 619
                $actualData[$name] = $value;
571
            }
572
        }
573 619
        return $actualData;
574
    }
575
576
    /**
577
     * Computes the changes that happened to a single document.
578
     *
579
     * Modifies/populates the following properties:
580
     *
581
     * {@link originalDocumentData}
582
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
583
     * then it was not fetched from the database and therefore we have no original
584
     * document data yet. All of the current document data is stored as the original document data.
585
     *
586
     * {@link documentChangeSets}
587
     * The changes detected on all properties of the document are stored there.
588
     * A change is a tuple array where the first entry is the old value and the second
589
     * entry is the new value of the property. Changesets are used by persisters
590
     * to INSERT/UPDATE the persistent document state.
591
     *
592
     * {@link documentUpdates}
593
     * If the document is already fully MANAGED (has been fetched from the database before)
594
     * and any changes to its properties are detected, then a reference to the document is stored
595
     * there to mark it for an update.
596
     */
597 615
    public function computeChangeSet(ClassMetadata $class, object $document) : void
598
    {
599 615
        if (! $class->isInheritanceTypeNone()) {
600 185
            $class = $this->dm->getClassMetadata(get_class($document));
601
        }
602
603
        // Fire PreFlush lifecycle callbacks
604 615
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
605 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
606
        }
607
608 615
        $this->computeOrRecomputeChangeSet($class, $document);
609 614
    }
610
611
    /**
612
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
613
     */
614 615
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
615
    {
616 615
        $oid           = spl_object_hash($document);
617 615
        $actualData    = $this->getDocumentActualData($document);
618 615
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
619 615
        if ($isNewDocument) {
620
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
621
            // These result in an INSERT.
622 614
            $this->originalDocumentData[$oid] = $actualData;
623 614
            $changeSet                        = [];
624 614
            foreach ($actualData as $propName => $actualValue) {
625
                /* At this PersistentCollection shouldn't be here, probably it
626
                 * was cloned and its ownership must be fixed
627
                 */
628 614
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
629
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
630
                    $actualValue           = $actualData[$propName];
631
                }
632
                // ignore inverse side of reference relationship
633 614
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
634 189
                    continue;
635
                }
636 614
                $changeSet[$propName] = [null, $actualValue];
637
            }
638 614
            $this->documentChangeSets[$oid] = $changeSet;
639
        } else {
640 297
            if ($class->isReadOnly) {
641 2
                return;
642
            }
643
            // Document is "fully" MANAGED: it was already fully persisted before
644
            // and we have a copy of the original data
645 295
            $originalData           = $this->originalDocumentData[$oid];
646 295
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
647 295
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
648 2
                $changeSet = $this->documentChangeSets[$oid];
649
            } else {
650 295
                $changeSet = [];
651
            }
652
653 295
            $gridFSMetadataProperty = null;
654
655 295
            if ($class->isFile) {
656
                try {
657 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
658 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
659 1
                } catch (MappingException $e) {
660
                }
661
            }
662
663 295
            foreach ($actualData as $propName => $actualValue) {
664
                // skip not saved fields
665 295
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
666 295
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
667 4
                    continue;
668
                }
669
670 294
                $orgValue = $originalData[$propName] ?? null;
671
672
                // skip if value has not changed
673 294
                if ($orgValue === $actualValue) {
674 292
                    if (! $actualValue instanceof PersistentCollectionInterface) {
675 292
                        continue;
676
                    }
677
678 206
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
679
                        // consider dirty collections as changed as well
680 182
                        continue;
681
                    }
682
                }
683
684
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
685 254
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
686 14
                    if ($orgValue !== null) {
687 8
                        $this->scheduleOrphanRemoval($orgValue);
688
                    }
689 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
690 14
                    continue;
691
                }
692
693
                // if owning side of reference-one relationship
694 247
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
695 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
696 1
                        $this->scheduleOrphanRemoval($orgValue);
697
                    }
698
699 13
                    $changeSet[$propName] = [$orgValue, $actualValue];
700 13
                    continue;
701
                }
702
703 240
                if ($isChangeTrackingNotify) {
704 3
                    continue;
705
                }
706
707
                // ignore inverse side of reference relationship
708 238
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
709 6
                    continue;
710
                }
711
712
                // Persistent collection was exchanged with the "originally"
713
                // created one. This can only mean it was cloned and replaced
714
                // on another document.
715 236
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
716 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
717
                }
718
719
                // if embed-many or reference-many relationship
720 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
721 125
                    $changeSet[$propName] = [$orgValue, $actualValue];
722
                    /* If original collection was exchanged with a non-empty value
723
                     * and $set will be issued, there is no need to $unset it first
724
                     */
725 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
726 31
                        continue;
727
                    }
728 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
729 18
                        $this->scheduleCollectionDeletion($orgValue);
730
                    }
731 104
                    continue;
732
                }
733
734
                // skip equivalent date values
735 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
736
                    /** @var DateType $dateType */
737 37
                    $dateType      = Type::getType('date');
738 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
739 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
740
741 37
                    $orgTimestamp    = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null;
742 37
                    $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null;
743
744 37
                    if ($orgTimestamp === $actualTimestamp) {
745 30
                        continue;
746
                    }
747
                }
748
749
                // regular field
750 131
                $changeSet[$propName] = [$orgValue, $actualValue];
751
            }
752 295
            if ($changeSet) {
753 243
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
754 19
                    ? $changeSet + $this->documentChangeSets[$oid]
755 240
                    : $changeSet;
756
757 243
                $this->originalDocumentData[$oid] = $actualData;
758 243
                $this->scheduleForUpdate($document);
759
            }
760
        }
761
762
        // Look for changes in associations of the document
763 615
        $associationMappings = array_filter(
764 615
            $class->associationMappings,
765
            static function ($assoc) {
766 474
                return empty($assoc['notSaved']);
767 615
            }
768
        );
769
770 615
        foreach ($associationMappings as $mapping) {
771 474
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
772
773 474
            if ($value === null) {
774 326
                continue;
775
            }
776
777 455
            $this->computeAssociationChanges($document, $mapping, $value);
778
779 454
            if (isset($mapping['reference'])) {
780 340
                continue;
781
            }
782
783 356
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
784
785 356
            foreach ($values as $obj) {
786 188
                $oid2 = spl_object_hash($obj);
787
788 188
                if (isset($this->documentChangeSets[$oid2])) {
789 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
790
                        // instance of $value is the same as it was previously otherwise there would be
791
                        // change set already in place
792 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
793
                    }
794
795 186
                    if (! $isNewDocument) {
796 87
                        $this->scheduleForUpdate($document);
797
                    }
798
799 186
                    break;
800
                }
801
            }
802
        }
803 614
    }
804
805
    /**
806
     * Computes all the changes that have been done to documents and collections
807
     * since the last commit and stores these changes in the _documentChangeSet map
808
     * temporarily for access by the persisters, until the UoW commit is finished.
809
     */
810 619
    public function computeChangeSets() : void
811
    {
812 619
        $this->computeScheduleInsertsChangeSets();
813 618
        $this->computeScheduleUpsertsChangeSets();
814
815
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
816 618
        foreach ($this->identityMap as $className => $documents) {
817 618
            $class = $this->dm->getClassMetadata($className);
818 618
            if ($class->isEmbeddedDocument) {
819
                /* we do not want to compute changes to embedded documents up front
820
                 * in case embedded document was replaced and its changeset
821
                 * would corrupt data. Embedded documents' change set will
822
                 * be calculated by reachability from owning document.
823
                 */
824 178
                continue;
825
            }
826
827
            // If change tracking is explicit or happens through notification, then only compute
828
            // changes on document of that type that are explicitly marked for synchronization.
829
            switch (true) {
830 618
                case $class->isChangeTrackingDeferredImplicit():
831 617
                    $documentsToProcess = $documents;
832 617
                    break;
833
834 4
                case isset($this->scheduledForSynchronization[$className]):
835 3
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
836 3
                    break;
837
838
                default:
839 4
                    $documentsToProcess = [];
840
            }
841
842 618
            foreach ($documentsToProcess as $document) {
843
                // Ignore uninitialized proxy objects
844 613
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
845 9
                    continue;
846
                }
847
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
848 613
                $oid = spl_object_hash($document);
849 613
                if (isset($this->documentInsertions[$oid])
850 338
                    || isset($this->documentUpserts[$oid])
851 292
                    || isset($this->documentDeletions[$oid])
852 613
                    || ! isset($this->documentStates[$oid])
853
                ) {
854 611
                    continue;
855
                }
856
857 292
                $this->computeChangeSet($class, $document);
858
            }
859
        }
860 618
    }
861
862
    /**
863
     * Computes the changes of an association.
864
     *
865
     * @param mixed $value The value of the association.
866
     *
867
     * @throws InvalidArgumentException
868
     */
869 455
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
870
    {
871 455
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
872 455
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
873 455
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
874
875 455
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
876 7
            return;
877
        }
878
879 454
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
880 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
881 254
                $this->scheduleCollectionUpdate($value);
882
            }
883
884 258
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
885 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
886 258
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
887 151
                $value->initialize();
888 151
                foreach ($value->getDeletedDocuments() as $orphan) {
889 25
                    $this->scheduleOrphanRemoval($orphan);
890
                }
891
            }
892
        }
893
894
        // Look through the documents, and in any of their associations,
895
        // for transient (new) documents, recursively. ("Persistence by reachability")
896
        // Unwrap. Uninitialized collections will simply be empty.
897 454
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
898
899 454
        $count = 0;
900 454
        foreach ($unwrappedValue as $key => $entry) {
901 368
            if (! is_object($entry)) {
902 1
                throw new InvalidArgumentException(
903 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
904
                );
905
            }
906
907 367
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
908
909 367
            $state = $this->getDocumentState($entry, self::STATE_NEW);
910
911
            // Handle "set" strategy for multi-level hierarchy
912 367
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
913 367
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
914
915 367
            $count++;
916
917
            switch ($state) {
918 367
                case self::STATE_NEW:
919 70
                    if (! $assoc['isCascadePersist']) {
920
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
921
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
922
                            . ' Explicitly persist the new document or configure cascading persist operations'
923
                            . ' on the relationship.');
924
                    }
925
926 70
                    $this->persistNew($targetClass, $entry);
927 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
928 70
                    $this->computeChangeSet($targetClass, $entry);
929 70
                    break;
930
931 363
                case self::STATE_MANAGED:
932 363
                    if ($targetClass->isEmbeddedDocument) {
933 179
                        [, $knownParent ] = $this->getParentAssociation($entry);
934 179
                        if ($knownParent && $knownParent !== $parentDocument) {
935 6
                            $entry = clone $entry;
936 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
937 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
938 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
939 3
                                $poid = spl_object_hash($parentDocument);
940 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
941 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
942
                                }
943
                            } else {
944
                                // must use unwrapped value to not trigger orphan removal
945 4
                                $unwrappedValue[$key] = $entry;
946
                            }
947 6
                            $this->persistNew($targetClass, $entry);
948
                        }
949 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
950 179
                        $this->computeChangeSet($targetClass, $entry);
951
                    }
952 363
                    break;
953
954 1
                case self::STATE_REMOVED:
955
                    // Consume the $value as array (it's either an array or an ArrayAccess)
956
                    // and remove the element from Collection.
957 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
958
                        unset($value[$key]);
959
                    }
960 1
                    break;
961
962
                case self::STATE_DETACHED:
963
                    // Can actually not happen right now as we assume STATE_NEW,
964
                    // so the exception will be raised from the DBAL layer (constraint violation).
965
                    throw new InvalidArgumentException('A detached document was found through a '
966
                        . 'relationship during cascading a persist operation.');
967
968
                default:
969
                    // MANAGED associated documents are already taken into account
970
                    // during changeset calculation anyway, since they are in the identity map.
971
            }
972
        }
973 453
    }
974
975
    /**
976
     * Computes the changeset of an individual document, independently of the
977
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
978
     *
979
     * The passed document must be a managed document. If the document already has a change set
980
     * because this method is invoked during a commit cycle then the change sets are added.
981
     * whereby changes detected in this method prevail.
982
     *
983
     * @throws InvalidArgumentException If the passed document is not MANAGED.
984
     */
985 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
986
    {
987
        // Ignore uninitialized proxy objects
988 19
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
989 1
            return;
990
        }
991
992 18
        $oid = spl_object_hash($document);
993
994 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
995
            throw new InvalidArgumentException('Document must be managed.');
996
        }
997
998 18
        if (! $class->isInheritanceTypeNone()) {
999 2
            $class = $this->dm->getClassMetadata(get_class($document));
1000
        }
1001
1002 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1003 18
    }
1004
1005
    /**
1006
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1007
     */
1008 644
    private function persistNew(ClassMetadata $class, object $document) : void
1009
    {
1010 644
        $this->lifecycleEventManager->prePersist($class, $document);
1011 644
        $oid    = spl_object_hash($document);
1012 644
        $upsert = false;
1013 644
        if ($class->identifier) {
1014 644
            $idValue = $class->getIdentifierValue($document);
1015 644
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1016
1017 644
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1018 3
                throw new InvalidArgumentException(sprintf(
1019 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1020 3
                    get_class($document)
1021
                ));
1022
            }
1023
1024 643
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1025 1
                throw new InvalidArgumentException(sprintf(
1026 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1027 1
                    get_class($document)
1028
                ));
1029
            }
1030
1031 642
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1032 563
                $idValue = $class->idGenerator->generate($this->dm, $document);
1033 563
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1034 563
                $class->setIdentifierValue($document, $idValue);
1035
            }
1036
1037 642
            $this->documentIdentifiers[$oid] = $idValue;
1038
        } else {
1039
            // this is for embedded documents without identifiers
1040 152
            $this->documentIdentifiers[$oid] = $oid;
1041
        }
1042
1043 642
        $this->documentStates[$oid] = self::STATE_MANAGED;
1044
1045 642
        if ($upsert) {
1046 89
            $this->scheduleForUpsert($class, $document);
1047
        } else {
1048 572
            $this->scheduleForInsert($class, $document);
1049
        }
1050 642
    }
1051
1052
    /**
1053
     * Executes all document insertions for documents of the specified type.
1054
     */
1055 533
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1056
    {
1057 533
        $persister = $this->getDocumentPersister($class->name);
1058
1059 533
        foreach ($documents as $oid => $document) {
1060 533
            $persister->addInsert($document);
1061 533
            unset($this->documentInsertions[$oid]);
1062
        }
1063
1064 533
        $persister->executeInserts($options);
1065
1066 522
        foreach ($documents as $document) {
1067 522
            $this->lifecycleEventManager->postPersist($class, $document);
1068
        }
1069 522
    }
1070
1071
    /**
1072
     * Executes all document upserts for documents of the specified type.
1073
     */
1074 86
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1075
    {
1076 86
        $persister = $this->getDocumentPersister($class->name);
1077
1078 86
        foreach ($documents as $oid => $document) {
1079 86
            $persister->addUpsert($document);
1080 86
            unset($this->documentUpserts[$oid]);
1081
        }
1082
1083 86
        $persister->executeUpserts($options);
1084
1085 86
        foreach ($documents as $document) {
1086 86
            $this->lifecycleEventManager->postPersist($class, $document);
1087
        }
1088 86
    }
1089
1090
    /**
1091
     * Executes all document updates for documents of the specified type.
1092
     */
1093 236
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1094
    {
1095 236
        if ($class->isReadOnly) {
1096
            return;
1097
        }
1098
1099 236
        $className = $class->name;
1100 236
        $persister = $this->getDocumentPersister($className);
1101
1102 236
        foreach ($documents as $oid => $document) {
1103 236
            $this->lifecycleEventManager->preUpdate($class, $document);
1104
1105 236
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1106 235
                $persister->update($document, $options);
1107
            }
1108
1109 229
            unset($this->documentUpdates[$oid]);
1110
1111 229
            $this->lifecycleEventManager->postUpdate($class, $document);
1112
        }
1113 228
    }
1114
1115
    /**
1116
     * Executes all document deletions for documents of the specified type.
1117
     */
1118 79
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1119
    {
1120 79
        $persister = $this->getDocumentPersister($class->name);
1121
1122 79
        foreach ($documents as $oid => $document) {
1123 79
            if (! $class->isEmbeddedDocument) {
1124 36
                $persister->delete($document, $options);
1125
            }
1126
            unset(
1127 77
                $this->documentDeletions[$oid],
1128 77
                $this->documentIdentifiers[$oid],
1129 77
                $this->originalDocumentData[$oid]
1130
            );
1131
1132
            // Clear snapshot information for any referenced PersistentCollection
1133
            // http://www.doctrine-project.org/jira/browse/MODM-95
1134 77
            foreach ($class->associationMappings as $fieldMapping) {
1135 53
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1136 38
                    continue;
1137
                }
1138
1139 33
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1140 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1141 7
                    continue;
1142
                }
1143
1144 29
                $value->clearSnapshot();
1145
            }
1146
1147
            // Document with this $oid after deletion treated as NEW, even if the $oid
1148
            // is obtained by a new document because the old one went out of scope.
1149 77
            $this->documentStates[$oid] = self::STATE_NEW;
1150
1151 77
            $this->lifecycleEventManager->postRemove($class, $document);
1152
        }
1153 77
    }
1154
1155
    /**
1156
     * Schedules a document for insertion into the database.
1157
     * If the document already has an identifier, it will be added to the
1158
     * identity map.
1159
     *
1160
     * @internal
1161
     *
1162
     * @throws InvalidArgumentException
1163
     */
1164 575
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1165
    {
1166 575
        $oid = spl_object_hash($document);
1167
1168 575
        if (isset($this->documentUpdates[$oid])) {
1169
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1170
        }
1171 575
        if (isset($this->documentDeletions[$oid])) {
1172
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1173
        }
1174 575
        if (isset($this->documentInsertions[$oid])) {
1175
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1176
        }
1177
1178 575
        $this->documentInsertions[$oid] = $document;
1179
1180 575
        if (! isset($this->documentIdentifiers[$oid])) {
1181 3
            return;
1182
        }
1183
1184 572
        $this->addToIdentityMap($document);
1185 572
    }
1186
1187
    /**
1188
     * Schedules a document for upsert into the database and adds it to the
1189
     * identity map
1190
     *
1191
     * @internal
1192
     *
1193
     * @throws InvalidArgumentException
1194
     */
1195 92
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1196
    {
1197 92
        $oid = spl_object_hash($document);
1198
1199 92
        if ($class->isEmbeddedDocument) {
1200
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1201
        }
1202 92
        if (isset($this->documentUpdates[$oid])) {
1203
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1204
        }
1205 92
        if (isset($this->documentDeletions[$oid])) {
1206
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1207
        }
1208 92
        if (isset($this->documentUpserts[$oid])) {
1209
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1210
        }
1211
1212 92
        $this->documentUpserts[$oid]     = $document;
1213 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1214 92
        $this->addToIdentityMap($document);
1215 92
    }
1216
1217
    /**
1218
     * Checks whether a document is scheduled for insertion.
1219
     */
1220 110
    public function isScheduledForInsert(object $document) : bool
1221
    {
1222 110
        return isset($this->documentInsertions[spl_object_hash($document)]);
1223
    }
1224
1225
    /**
1226
     * Checks whether a document is scheduled for upsert.
1227
     */
1228 5
    public function isScheduledForUpsert(object $document) : bool
1229
    {
1230 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1231
    }
1232
1233
    /**
1234
     * Schedules a document for being updated.
1235
     *
1236
     * @internal
1237
     *
1238
     * @throws InvalidArgumentException
1239
     */
1240 245
    public function scheduleForUpdate(object $document) : void
1241
    {
1242 245
        $oid = spl_object_hash($document);
1243 245
        if (! isset($this->documentIdentifiers[$oid])) {
1244
            throw new InvalidArgumentException('Document has no identity.');
1245
        }
1246
1247 245
        if (isset($this->documentDeletions[$oid])) {
1248
            throw new InvalidArgumentException('Document is removed.');
1249
        }
1250
1251 245
        if (isset($this->documentUpdates[$oid])
1252 245
            || isset($this->documentInsertions[$oid])
1253 245
            || isset($this->documentUpserts[$oid])) {
1254 104
            return;
1255
        }
1256
1257 243
        $this->documentUpdates[$oid] = $document;
1258 243
    }
1259
1260
    /**
1261
     * Checks whether a document is registered as dirty in the unit of work.
1262
     * Note: Is not very useful currently as dirty documents are only registered
1263
     * at commit time.
1264
     */
1265 21
    public function isScheduledForUpdate(object $document) : bool
1266
    {
1267 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1268
    }
1269
1270
    /**
1271
     * Checks whether a document is registered to be checked in the unit of work.
1272
     */
1273 1
    public function isScheduledForSynchronization(object $document) : bool
1274
    {
1275 1
        $class = $this->dm->getClassMetadata(get_class($document));
1276 1
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1277
    }
1278
1279
    /**
1280
     * Schedules a document for deletion.
1281
     *
1282
     * @internal
1283
     */
1284 84
    public function scheduleForDelete(object $document) : void
1285
    {
1286 84
        $oid = spl_object_hash($document);
1287
1288 84
        if (isset($this->documentInsertions[$oid])) {
1289 2
            if ($this->isInIdentityMap($document)) {
1290 2
                $this->removeFromIdentityMap($document);
1291
            }
1292 2
            unset($this->documentInsertions[$oid]);
1293 2
            return; // document has not been persisted yet, so nothing more to do.
1294
        }
1295
1296 83
        if (! $this->isInIdentityMap($document)) {
1297 2
            return; // ignore
1298
        }
1299
1300 82
        $this->removeFromIdentityMap($document);
1301 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1302
1303 82
        if (isset($this->documentUpdates[$oid])) {
1304
            unset($this->documentUpdates[$oid]);
1305
        }
1306 82
        if (isset($this->documentDeletions[$oid])) {
1307
            return;
1308
        }
1309
1310 82
        $this->documentDeletions[$oid] = $document;
1311 82
    }
1312
1313
    /**
1314
     * Checks whether a document is registered as removed/deleted with the unit
1315
     * of work.
1316
     */
1317 5
    public function isScheduledForDelete(object $document) : bool
1318
    {
1319 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1320
    }
1321
1322
    /**
1323
     * Checks whether a document is scheduled for insertion, update or deletion.
1324
     *
1325
     * @internal
1326
     */
1327 257
    public function isDocumentScheduled(object $document) : bool
1328
    {
1329 257
        $oid = spl_object_hash($document);
1330 257
        return isset($this->documentInsertions[$oid]) ||
1331 139
            isset($this->documentUpserts[$oid]) ||
1332 129
            isset($this->documentUpdates[$oid]) ||
1333 257
            isset($this->documentDeletions[$oid]);
1334
    }
1335
1336
    /**
1337
     * Registers a document in the identity map.
1338
     *
1339
     * Note that documents in a hierarchy are registered with the class name of
1340
     * the root document. Identifiers are serialized before being used as array
1341
     * keys to allow differentiation of equal, but not identical, values.
1342
     *
1343
     * @internal
1344
     */
1345 682
    public function addToIdentityMap(object $document) : bool
1346
    {
1347 682
        $class = $this->dm->getClassMetadata(get_class($document));
1348 682
        $id    = $this->getIdForIdentityMap($document);
1349
1350 682
        if (isset($this->identityMap[$class->name][$id])) {
1351 44
            return false;
1352
        }
1353
1354 682
        $this->identityMap[$class->name][$id] = $document;
1355
1356 682
        if ($document instanceof NotifyPropertyChanged &&
1357 682
            ( ! $document instanceof GhostObjectInterface || $document->isProxyInitialized())) {
1358 3
            $document->addPropertyChangedListener($this);
1359
        }
1360
1361 682
        return true;
1362
    }
1363
1364
    /**
1365
     * Gets the state of a document with regard to the current unit of work.
1366
     *
1367
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1368
     *                         This parameter can be set to improve performance of document state detection
1369
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1370
     *                         is either known or does not matter for the caller of the method.
1371
     */
1372 648
    public function getDocumentState(object $document, ?int $assume = null) : int
1373
    {
1374 648
        $oid = spl_object_hash($document);
1375
1376 648
        if (isset($this->documentStates[$oid])) {
1377 407
            return $this->documentStates[$oid];
1378
        }
1379
1380 647
        $class = $this->dm->getClassMetadata(get_class($document));
1381
1382 647
        if ($class->isEmbeddedDocument) {
1383 192
            return self::STATE_NEW;
1384
        }
1385
1386 644
        if ($assume !== null) {
1387 642
            return $assume;
1388
        }
1389
1390
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1391
         * known. Note that you cannot remember the NEW or DETACHED state in
1392
         * _documentStates since the UoW does not hold references to such
1393
         * objects and the object hash can be reused. More generally, because
1394
         * the state may "change" between NEW/DETACHED without the UoW being
1395
         * aware of it.
1396
         */
1397 3
        $id = $class->getIdentifierObject($document);
1398
1399 3
        if ($id === null) {
1400 2
            return self::STATE_NEW;
1401
        }
1402
1403
        // Check for a version field, if available, to avoid a DB lookup.
1404 2
        if ($class->isVersioned && $class->versionField !== null) {
1405
            return $class->getFieldValue($document, $class->versionField)
1406
                ? self::STATE_DETACHED
1407
                : self::STATE_NEW;
1408
        }
1409
1410
        // Last try before DB lookup: check the identity map.
1411 2
        if ($this->tryGetById($id, $class)) {
1412 1
            return self::STATE_DETACHED;
1413
        }
1414
1415
        // DB lookup
1416 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1417 1
            return self::STATE_DETACHED;
1418
        }
1419
1420 1
        return self::STATE_NEW;
1421
    }
1422
1423
    /**
1424
     * Removes a document from the identity map. This effectively detaches the
1425
     * document from the persistence management of Doctrine.
1426
     *
1427
     * @internal
1428
     *
1429
     * @throws InvalidArgumentException
1430
     */
1431 96
    public function removeFromIdentityMap(object $document) : bool
1432
    {
1433 96
        $oid = spl_object_hash($document);
1434
1435
        // Check if id is registered first
1436 96
        if (! isset($this->documentIdentifiers[$oid])) {
1437
            return false;
1438
        }
1439
1440 96
        $class = $this->dm->getClassMetadata(get_class($document));
1441 96
        $id    = $this->getIdForIdentityMap($document);
1442
1443 96
        if (isset($this->identityMap[$class->name][$id])) {
1444 96
            unset($this->identityMap[$class->name][$id]);
1445 96
            $this->documentStates[$oid] = self::STATE_DETACHED;
1446 96
            return true;
1447
        }
1448
1449
        return false;
1450
    }
1451
1452
    /**
1453
     * Gets a document in the identity map by its identifier hash.
1454
     *
1455
     * @internal
1456
     *
1457
     * @param mixed $id Document identifier
1458
     *
1459
     * @throws InvalidArgumentException If the class does not have an identifier.
1460
     */
1461 37
    public function getById($id, ClassMetadata $class) : object
1462
    {
1463 37
        if (! $class->identifier) {
1464
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1465
        }
1466
1467 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1468
1469 37
        return $this->identityMap[$class->name][$serializedId];
1470
    }
1471
1472
    /**
1473
     * Tries to get a document by its identifier hash. If no document is found
1474
     * for the given hash, FALSE is returned.
1475
     *
1476
     * @internal
1477
     *
1478
     * @param mixed $id Document identifier
1479
     *
1480
     * @return mixed The found document or FALSE.
1481
     *
1482
     * @throws InvalidArgumentException If the class does not have an identifier.
1483
     */
1484 307
    public function tryGetById($id, ClassMetadata $class)
1485
    {
1486 307
        if (! $class->identifier) {
1487
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1488
        }
1489
1490 307
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1491
1492 307
        return $this->identityMap[$class->name][$serializedId] ?? false;
1493
    }
1494
1495
    /**
1496
     * Schedules a document for dirty-checking at commit-time.
1497
     *
1498
     * @internal
1499
     */
1500 3
    public function scheduleForSynchronization(object $document) : void
1501
    {
1502 3
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1503 3
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1504 3
    }
1505
1506
    /**
1507
     * Checks whether a document is registered in the identity map.
1508
     *
1509
     * @internal
1510
     */
1511 92
    public function isInIdentityMap(object $document) : bool
1512
    {
1513 92
        $oid = spl_object_hash($document);
1514
1515 92
        if (! isset($this->documentIdentifiers[$oid])) {
1516 6
            return false;
1517
        }
1518
1519 90
        $class = $this->dm->getClassMetadata(get_class($document));
1520 90
        $id    = $this->getIdForIdentityMap($document);
1521
1522 90
        return isset($this->identityMap[$class->name][$id]);
1523
    }
1524
1525 682
    private function getIdForIdentityMap(object $document) : string
1526
    {
1527 682
        $class = $this->dm->getClassMetadata(get_class($document));
1528
1529 682
        if (! $class->identifier) {
1530 158
            $id = spl_object_hash($document);
1531
        } else {
1532 681
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1533 681
            $id = serialize($class->getDatabaseIdentifierValue($id));
1534
        }
1535
1536 682
        return $id;
1537
    }
1538
1539
    /**
1540
     * Checks whether an identifier exists in the identity map.
1541
     *
1542
     * @internal
1543
     */
1544
    public function containsId($id, string $rootClassName) : bool
1545
    {
1546
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1547
    }
1548
1549
    /**
1550
     * Persists a document as part of the current unit of work.
1551
     *
1552
     * @internal
1553
     *
1554
     * @throws MongoDBException If trying to persist MappedSuperclass.
1555
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1556
     */
1557 645
    public function persist(object $document) : void
1558
    {
1559 645
        $class = $this->dm->getClassMetadata(get_class($document));
1560 645
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1561 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1562
        }
1563 644
        $visited = [];
1564 644
        $this->doPersist($document, $visited);
1565 639
    }
1566
1567
    /**
1568
     * Saves a document as part of the current unit of work.
1569
     * This method is internally called during save() cascades as it tracks
1570
     * the already visited documents to prevent infinite recursions.
1571
     *
1572
     * NOTE: This method always considers documents that are not yet known to
1573
     * this UnitOfWork as NEW.
1574
     *
1575
     * @throws InvalidArgumentException
1576
     * @throws MongoDBException
1577
     */
1578 644
    private function doPersist(object $document, array &$visited) : void
1579
    {
1580 644
        $oid = spl_object_hash($document);
1581 644
        if (isset($visited[$oid])) {
1582 25
            return; // Prevent infinite recursion
1583
        }
1584
1585 644
        $visited[$oid] = $document; // Mark visited
1586
1587 644
        $class = $this->dm->getClassMetadata(get_class($document));
1588
1589 644
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1590
        switch ($documentState) {
1591 644
            case self::STATE_MANAGED:
1592
                // Nothing to do, except if policy is "deferred explicit"
1593 57
                if ($class->isChangeTrackingDeferredExplicit()) {
1594
                    $this->scheduleForSynchronization($document);
1595
                }
1596 57
                break;
1597 644
            case self::STATE_NEW:
1598 644
                if ($class->isFile) {
1599 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1600
                }
1601
1602 643
                $this->persistNew($class, $document);
1603 641
                break;
1604
1605 2
            case self::STATE_REMOVED:
1606
                // Document becomes managed again
1607 2
                unset($this->documentDeletions[$oid]);
1608
1609 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1610 2
                break;
1611
1612
            case self::STATE_DETACHED:
1613
                throw new InvalidArgumentException(
1614
                    'Behavior of persist() for a detached document is not yet defined.'
1615
                );
1616
1617
            default:
1618
                throw MongoDBException::invalidDocumentState($documentState);
1619
        }
1620
1621 641
        $this->cascadePersist($document, $visited);
1622 639
    }
1623
1624
    /**
1625
     * Deletes a document as part of the current unit of work.
1626
     *
1627
     * @internal
1628
     */
1629 83
    public function remove(object $document)
1630
    {
1631 83
        $visited = [];
1632 83
        $this->doRemove($document, $visited);
1633 83
    }
1634
1635
    /**
1636
     * Deletes a document as part of the current unit of work.
1637
     *
1638
     * This method is internally called during delete() cascades as it tracks
1639
     * the already visited documents to prevent infinite recursions.
1640
     *
1641
     * @throws MongoDBException
1642
     */
1643 83
    private function doRemove(object $document, array &$visited) : void
1644
    {
1645 83
        $oid = spl_object_hash($document);
1646 83
        if (isset($visited[$oid])) {
1647 1
            return; // Prevent infinite recursion
1648
        }
1649
1650 83
        $visited[$oid] = $document; // mark visited
1651
1652
        /* Cascade first, because scheduleForDelete() removes the entity from
1653
         * the identity map, which can cause problems when a lazy Proxy has to
1654
         * be initialized for the cascade operation.
1655
         */
1656 83
        $this->cascadeRemove($document, $visited);
1657
1658 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1659 83
        $documentState = $this->getDocumentState($document);
1660
        switch ($documentState) {
1661 83
            case self::STATE_NEW:
1662 83
            case self::STATE_REMOVED:
1663
                // nothing to do
1664 1
                break;
1665 83
            case self::STATE_MANAGED:
1666 83
                $this->lifecycleEventManager->preRemove($class, $document);
1667 83
                $this->scheduleForDelete($document);
1668 83
                break;
1669
            case self::STATE_DETACHED:
1670
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1671
            default:
1672
                throw MongoDBException::invalidDocumentState($documentState);
1673
        }
1674 83
    }
1675
1676
    /**
1677
     * Merges the state of the given detached document into this UnitOfWork.
1678
     *
1679
     * @internal
1680
     */
1681 11
    public function merge(object $document) : object
1682
    {
1683 11
        $visited = [];
1684
1685 11
        return $this->doMerge($document, $visited);
1686
    }
1687
1688
    /**
1689
     * Executes a merge operation on a document.
1690
     *
1691
     * @throws InvalidArgumentException If the entity instance is NEW.
1692
     * @throws LockException If the document uses optimistic locking through a
1693
     *                       version attribute and the version check against the
1694
     *                       managed copy fails.
1695
     */
1696 11
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1697
    {
1698 11
        $oid = spl_object_hash($document);
1699
1700 11
        if (isset($visited[$oid])) {
1701 1
            return $visited[$oid]; // Prevent infinite recursion
1702
        }
1703
1704 11
        $visited[$oid] = $document; // mark visited
1705
1706 11
        $class = $this->dm->getClassMetadata(get_class($document));
1707
1708
        /* First we assume DETACHED, although it can still be NEW but we can
1709
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1710
         * an identity, we need to fetch it from the DB anyway in order to
1711
         * merge. MANAGED documents are ignored by the merge operation.
1712
         */
1713 11
        $managedCopy = $document;
1714
1715 11
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1716 11
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
1717
                $document->initializeProxy();
1718
            }
1719
1720 11
            $identifier = $class->getIdentifier();
1721
            // We always have one element in the identifier array but it might be null
1722 11
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1723 11
            $managedCopy = null;
1724
1725
            // Try to fetch document from the database
1726 11
            if (! $class->isEmbeddedDocument && $id !== null) {
1727 11
                $managedCopy = $this->dm->find($class->name, $id);
1728
1729
                // Managed copy may be removed in which case we can't merge
1730 11
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1731
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1732
                }
1733
1734 11
                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
1735
                    $managedCopy->initializeProxy();
1736
                }
1737
            }
1738
1739 11
            if ($managedCopy === null) {
1740
                // Create a new managed instance
1741 4
                $managedCopy = $class->newInstance();
1742 4
                if ($id !== null) {
1743 3
                    $class->setIdentifierValue($managedCopy, $id);
1744
                }
1745 4
                $this->persistNew($class, $managedCopy);
1746
            }
1747
1748 11
            if ($class->isVersioned) {
1749
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1750
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1751
1752
                // Throw exception if versions don't match
1753
                if ($managedCopyVersion !== $documentVersion) {
1754
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1755
                }
1756
            }
1757
1758
            // Merge state of $document into existing (managed) document
1759 11
            foreach ($class->reflClass->getProperties() as $prop) {
1760 11
                $name = $prop->name;
1761 11
                $prop->setAccessible(true);
1762 11
                if (! isset($class->associationMappings[$name])) {
1763 11
                    if (! $class->isIdentifier($name)) {
1764 11
                        $prop->setValue($managedCopy, $prop->getValue($document));
1765
                    }
1766
                } else {
1767 11
                    $assoc2 = $class->associationMappings[$name];
1768
1769 11
                    if ($assoc2['type'] === 'one') {
1770 5
                        $other = $prop->getValue($document);
1771
1772 5
                        if ($other === null) {
1773 2
                            $prop->setValue($managedCopy, null);
1774 4
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
1775
                            // Do not merge fields marked lazy that have not been fetched
1776
                            continue;
1777 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1778
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1779
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1780
                                /** @var ClassMetadata $targetClass */
1781
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1782
                                $relatedId   = $targetClass->getIdentifierObject($other);
1783
1784
                                if ($targetClass->subClasses) {
1785
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1786
                                } else {
1787
                                    $other = $this
1788
                                        ->dm
1789
                                        ->getProxyFactory()
1790
                                        ->getProxy($targetClass, $relatedId);
1791
                                    $this->registerManaged($other, $relatedId, []);
1792
                                }
1793
                            }
1794
1795 5
                            $prop->setValue($managedCopy, $other);
1796
                        }
1797
                    } else {
1798 10
                        $mergeCol = $prop->getValue($document);
1799
1800 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1801
                            /* Do not merge fields marked lazy that have not
1802
                             * been fetched. Keep the lazy persistent collection
1803
                             * of the managed copy.
1804
                             */
1805 3
                            continue;
1806
                        }
1807
1808 10
                        $managedCol = $prop->getValue($managedCopy);
1809
1810 10
                        if (! $managedCol) {
1811 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1812 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1813 1
                            $prop->setValue($managedCopy, $managedCol);
1814 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1815
                        }
1816
1817
                        /* Note: do not process association's target documents.
1818
                         * They will be handled during the cascade. Initialize
1819
                         * and, if necessary, clear $managedCol for now.
1820
                         */
1821 10
                        if ($assoc2['isCascadeMerge']) {
1822 10
                            $managedCol->initialize();
1823
1824
                            // If $managedCol differs from the merged collection, clear and set dirty
1825 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1826 3
                                $managedCol->unwrap()->clear();
1827 3
                                $managedCol->setDirty(true);
1828
1829 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1830
                                    $this->scheduleForSynchronization($managedCopy);
1831
                                }
1832
                            }
1833
                        }
1834
                    }
1835
                }
1836
1837 11
                if (! $class->isChangeTrackingNotify()) {
1838 11
                    continue;
1839
                }
1840
1841
                // Just treat all properties as changed, there is no other choice.
1842
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1843
            }
1844
1845 11
            if ($class->isChangeTrackingDeferredExplicit()) {
1846
                $this->scheduleForSynchronization($document);
1847
            }
1848
        }
1849
1850 11
        if ($prevManagedCopy !== null) {
1851 5
            $assocField = $assoc['fieldName'];
1852 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1853
1854 5
            if ($assoc['type'] === 'one') {
1855 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1856
            } else {
1857 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1858
1859 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1860 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1861
                }
1862
            }
1863
        }
1864
1865
        // Mark the managed copy visited as well
1866 11
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1867
1868 11
        $this->cascadeMerge($document, $managedCopy, $visited);
1869
1870 11
        return $managedCopy;
1871
    }
1872
1873
    /**
1874
     * Detaches a document from the persistence management. It's persistence will
1875
     * no longer be managed by Doctrine.
1876
     *
1877
     * @internal
1878
     */
1879 11
    public function detach(object $document) : void
1880
    {
1881 11
        $visited = [];
1882 11
        $this->doDetach($document, $visited);
1883 11
    }
1884
1885
    /**
1886
     * Executes a detach operation on the given document.
1887
     *
1888
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1889
     */
1890 17
    private function doDetach(object $document, array &$visited) : void
1891
    {
1892 17
        $oid = spl_object_hash($document);
1893 17
        if (isset($visited[$oid])) {
1894 3
            return; // Prevent infinite recursion
1895
        }
1896
1897 17
        $visited[$oid] = $document; // mark visited
1898
1899 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1900 17
            case self::STATE_MANAGED:
1901 17
                $this->removeFromIdentityMap($document);
1902
                unset(
1903 17
                    $this->documentInsertions[$oid],
1904 17
                    $this->documentUpdates[$oid],
1905 17
                    $this->documentDeletions[$oid],
1906 17
                    $this->documentIdentifiers[$oid],
1907 17
                    $this->documentStates[$oid],
1908 17
                    $this->originalDocumentData[$oid],
1909 17
                    $this->parentAssociations[$oid],
1910 17
                    $this->documentUpserts[$oid],
1911 17
                    $this->hasScheduledCollections[$oid],
1912 17
                    $this->embeddedDocumentsRegistry[$oid]
1913
                );
1914 17
                break;
1915 3
            case self::STATE_NEW:
1916 3
            case self::STATE_DETACHED:
1917 3
                return;
1918
        }
1919
1920 17
        $this->cascadeDetach($document, $visited);
1921 17
    }
1922
1923
    /**
1924
     * Refreshes the state of the given document from the database, overwriting
1925
     * any local, unpersisted changes.
1926
     *
1927
     * @internal
1928
     *
1929
     * @throws InvalidArgumentException If the document is not MANAGED.
1930
     */
1931 24
    public function refresh(object $document) : void
1932
    {
1933 24
        $visited = [];
1934 24
        $this->doRefresh($document, $visited);
1935 23
    }
1936
1937
    /**
1938
     * Executes a refresh operation on a document.
1939
     *
1940
     * @throws InvalidArgumentException If the document is not MANAGED.
1941
     */
1942 24
    private function doRefresh(object $document, array &$visited) : void
1943
    {
1944 24
        $oid = spl_object_hash($document);
1945 24
        if (isset($visited[$oid])) {
1946
            return; // Prevent infinite recursion
1947
        }
1948
1949 24
        $visited[$oid] = $document; // mark visited
1950
1951 24
        $class = $this->dm->getClassMetadata(get_class($document));
1952
1953 24
        if (! $class->isEmbeddedDocument) {
1954 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1955 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1956
            }
1957
1958 23
            $this->getDocumentPersister($class->name)->refresh($document);
1959
        }
1960
1961 23
        $this->cascadeRefresh($document, $visited);
1962 23
    }
1963
1964
    /**
1965
     * Cascades a refresh operation to associated documents.
1966
     */
1967 23
    private function cascadeRefresh(object $document, array &$visited) : void
1968
    {
1969 23
        $class = $this->dm->getClassMetadata(get_class($document));
1970
1971 23
        $associationMappings = array_filter(
1972 23
            $class->associationMappings,
1973
            static function ($assoc) {
1974 18
                return $assoc['isCascadeRefresh'];
1975 23
            }
1976
        );
1977
1978 23
        foreach ($associationMappings as $mapping) {
1979 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1980 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
1981 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1982
                    // Unwrap so that foreach() does not initialize
1983 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1984
                }
1985 15
                foreach ($relatedDocuments as $relatedDocument) {
1986
                    $this->doRefresh($relatedDocument, $visited);
1987
                }
1988 10
            } elseif ($relatedDocuments !== null) {
1989 2
                $this->doRefresh($relatedDocuments, $visited);
1990
            }
1991
        }
1992 23
    }
1993
1994
    /**
1995
     * Cascades a detach operation to associated documents.
1996
     */
1997 17
    private function cascadeDetach(object $document, array &$visited) : void
1998
    {
1999 17
        $class = $this->dm->getClassMetadata(get_class($document));
2000 17
        foreach ($class->fieldMappings as $mapping) {
2001 17
            if (! $mapping['isCascadeDetach']) {
2002 17
                continue;
2003
            }
2004 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2005 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2006 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2007
                    // Unwrap so that foreach() does not initialize
2008 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2009
                }
2010 11
                foreach ($relatedDocuments as $relatedDocument) {
2011 8
                    $this->doDetach($relatedDocument, $visited);
2012
                }
2013 11
            } elseif ($relatedDocuments !== null) {
2014 8
                $this->doDetach($relatedDocuments, $visited);
2015
            }
2016
        }
2017 17
    }
2018
    /**
2019
     * Cascades a merge operation to associated documents.
2020
     */
2021 11
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2022
    {
2023 11
        $class = $this->dm->getClassMetadata(get_class($document));
2024
2025 11
        $associationMappings = array_filter(
2026 11
            $class->associationMappings,
2027
            static function ($assoc) {
2028 11
                return $assoc['isCascadeMerge'];
2029 11
            }
2030
        );
2031
2032 11
        foreach ($associationMappings as $assoc) {
2033 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2034
2035 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2036 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2037
                    // Collections are the same, so there is nothing to do
2038 1
                    continue;
2039
                }
2040
2041 8
                foreach ($relatedDocuments as $relatedDocument) {
2042 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2043
                }
2044 6
            } elseif ($relatedDocuments !== null) {
2045 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2046
            }
2047
        }
2048 11
    }
2049
2050
    /**
2051
     * Cascades the save operation to associated documents.
2052
     */
2053 641
    private function cascadePersist(object $document, array &$visited) : void
2054
    {
2055 641
        $class = $this->dm->getClassMetadata(get_class($document));
2056
2057 641
        $associationMappings = array_filter(
2058 641
            $class->associationMappings,
2059
            static function ($assoc) {
2060 496
                return $assoc['isCascadePersist'];
2061 641
            }
2062
        );
2063
2064 641
        foreach ($associationMappings as $fieldName => $mapping) {
2065 443
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2066
2067 443
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2068 372
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2069 15
                    if ($relatedDocuments->getOwner() !== $document) {
2070 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2071
                    }
2072
                    // Unwrap so that foreach() does not initialize
2073 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2074
                }
2075
2076 372
                $count = 0;
2077 372
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2078 201
                    if (! empty($mapping['embedded'])) {
2079 130
                        [, $knownParent ] = $this->getParentAssociation($relatedDocument);
2080 130
                        if ($knownParent && $knownParent !== $document) {
2081 1
                            $relatedDocument               = clone $relatedDocument;
2082 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2083
                        }
2084 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2085 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2086
                    }
2087 201
                    $this->doPersist($relatedDocument, $visited);
2088
                }
2089 346
            } elseif ($relatedDocuments !== null) {
2090 129
                if (! empty($mapping['embedded'])) {
2091 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2092 69
                    if ($knownParent && $knownParent !== $document) {
2093 3
                        $relatedDocuments = clone $relatedDocuments;
2094 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2095
                    }
2096 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2097
                }
2098 129
                $this->doPersist($relatedDocuments, $visited);
2099
            }
2100
        }
2101 639
    }
2102
2103
    /**
2104
     * Cascades the delete operation to associated documents.
2105
     */
2106 83
    private function cascadeRemove(object $document, array &$visited) : void
2107
    {
2108 83
        $class = $this->dm->getClassMetadata(get_class($document));
2109 83
        foreach ($class->fieldMappings as $mapping) {
2110 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2111 82
                continue;
2112
            }
2113 43
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
2114 3
                $document->initializeProxy();
2115
            }
2116
2117 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2118 43
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2119
                // If its a PersistentCollection initialization is intended! No unwrap!
2120 31
                foreach ($relatedDocuments as $relatedDocument) {
2121 15
                    $this->doRemove($relatedDocument, $visited);
2122
                }
2123 27
            } elseif ($relatedDocuments !== null) {
2124 14
                $this->doRemove($relatedDocuments, $visited);
2125
            }
2126
        }
2127 83
    }
2128
2129
    /**
2130
     * Acquire a lock on the given document.
2131
     *
2132
     * @internal
2133
     *
2134
     * @throws LockException
2135
     * @throws InvalidArgumentException
2136
     */
2137 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2138
    {
2139 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2140 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2141
        }
2142
2143 7
        $documentName = get_class($document);
2144 7
        $class        = $this->dm->getClassMetadata($documentName);
2145
2146 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2147 2
            if (! $class->isVersioned) {
2148 1
                throw LockException::notVersioned($documentName);
2149
            }
2150
2151 1
            if ($lockVersion !== null) {
2152 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2153 1
                if ($documentVersion !== $lockVersion) {
2154 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2155
                }
2156
            }
2157 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2158 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2159
        }
2160 5
    }
2161
2162
    /**
2163
     * Releases a lock on the given document.
2164
     *
2165
     * @internal
2166
     *
2167
     * @throws InvalidArgumentException
2168
     */
2169 1
    public function unlock(object $document) : void
2170
    {
2171 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2172
            throw new InvalidArgumentException('Document is not MANAGED.');
2173
        }
2174 1
        $documentName = get_class($document);
2175 1
        $this->getDocumentPersister($documentName)->unlock($document);
2176 1
    }
2177
2178
    /**
2179
     * Clears the UnitOfWork.
2180
     *
2181
     * @internal
2182
     */
2183 379
    public function clear(?string $documentName = null) : void
2184
    {
2185 379
        if ($documentName === null) {
2186 373
            $this->identityMap                 =
2187 373
            $this->documentIdentifiers         =
2188 373
            $this->originalDocumentData        =
2189 373
            $this->documentChangeSets          =
2190 373
            $this->documentStates              =
2191 373
            $this->scheduledForSynchronization =
2192 373
            $this->documentInsertions          =
2193 373
            $this->documentUpserts             =
2194 373
            $this->documentUpdates             =
2195 373
            $this->documentDeletions           =
2196 373
            $this->collectionUpdates           =
2197 373
            $this->collectionDeletions         =
2198 373
            $this->parentAssociations          =
2199 373
            $this->embeddedDocumentsRegistry   =
2200 373
            $this->orphanRemovals              =
2201 373
            $this->hasScheduledCollections     = [];
2202
        } else {
2203 6
            $visited = [];
2204 6
            foreach ($this->identityMap as $className => $documents) {
2205 6
                if ($className !== $documentName) {
2206 3
                    continue;
2207
                }
2208
2209 6
                foreach ($documents as $document) {
2210 6
                    $this->doDetach($document, $visited);
2211
                }
2212
            }
2213
        }
2214
2215 379
        if (! $this->evm->hasListeners(Events::onClear)) {
2216 379
            return;
2217
        }
2218
2219
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2220
    }
2221
2222
    /**
2223
     * Schedules an embedded document for removal. The remove() operation will be
2224
     * invoked on that document at the beginning of the next commit of this
2225
     * UnitOfWork.
2226
     *
2227
     * @internal
2228
     */
2229 58
    public function scheduleOrphanRemoval(object $document) : void
2230
    {
2231 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2232 58
    }
2233
2234
    /**
2235
     * Unschedules an embedded or referenced object for removal.
2236
     *
2237
     * @internal
2238
     */
2239 123
    public function unscheduleOrphanRemoval(object $document) : void
2240
    {
2241 123
        $oid = spl_object_hash($document);
2242 123
        unset($this->orphanRemovals[$oid]);
2243 123
    }
2244
2245
    /**
2246
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2247
     *  1) sets owner if it was cloned
2248
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2249
     *  3) NOP if state is OK
2250
     * Returned collection should be used from now on (only important with 2nd point)
2251
     */
2252 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2253
    {
2254 8
        $owner = $coll->getOwner();
2255 8
        if ($owner === null) { // cloned
2256 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2257 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2258 2
            if (! $coll->isInitialized()) {
2259 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2260
            }
2261 2
            $newValue = clone $coll;
2262 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2263 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2264 2
            if ($this->isScheduledForUpdate($document)) {
2265
                // @todo following line should be superfluous once collections are stored in change sets
2266
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2267
            }
2268 2
            return $newValue;
2269
        }
2270 6
        return $coll;
2271
    }
2272
2273
    /**
2274
     * Schedules a complete collection for removal when this UnitOfWork commits.
2275
     *
2276
     * @internal
2277
     */
2278 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2279
    {
2280 47
        $oid = spl_object_hash($coll);
2281 47
        unset($this->collectionUpdates[$oid]);
2282 47
        if (isset($this->collectionDeletions[$oid])) {
2283
            return;
2284
        }
2285
2286 47
        $this->collectionDeletions[$oid] = $coll;
2287 47
        $this->scheduleCollectionOwner($coll);
2288 47
    }
2289
2290
    /**
2291
     * Checks whether a PersistentCollection is scheduled for deletion.
2292
     *
2293
     * @internal
2294
     */
2295 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2296
    {
2297 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2298
    }
2299
2300
    /**
2301
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2302
     *
2303
     * @internal
2304
     */
2305 227
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2306
    {
2307 227
        if ($coll->getOwner() === null) {
2308
            return;
2309
        }
2310
2311 227
        $oid = spl_object_hash($coll);
2312 227
        if (! isset($this->collectionDeletions[$oid])) {
2313 227
            return;
2314
        }
2315
2316 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2317 14
        unset($this->collectionDeletions[$oid]);
2318 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2319 14
    }
2320
2321
    /**
2322
     * Schedules a collection for update when this UnitOfWork commits.
2323
     *
2324
     * @internal
2325
     */
2326 254
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2327
    {
2328 254
        $mapping = $coll->getMapping();
2329 254
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2330
            /* There is no need to $unset collection if it will be $set later
2331
             * This is NOP if collection is not scheduled for deletion
2332
             */
2333 44
            $this->unscheduleCollectionDeletion($coll);
2334
        }
2335 254
        $oid = spl_object_hash($coll);
2336 254
        if (isset($this->collectionUpdates[$oid])) {
2337 11
            return;
2338
        }
2339
2340 254
        $this->collectionUpdates[$oid] = $coll;
2341 254
        $this->scheduleCollectionOwner($coll);
2342 254
    }
2343
2344
    /**
2345
     * Unschedules a collection from being updated when this UnitOfWork commits.
2346
     *
2347
     * @internal
2348
     */
2349 227
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2350
    {
2351 227
        if ($coll->getOwner() === null) {
2352
            return;
2353
        }
2354
2355 227
        $oid = spl_object_hash($coll);
2356 227
        if (! isset($this->collectionUpdates[$oid])) {
2357 52
            return;
2358
        }
2359
2360 216
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2361 216
        unset($this->collectionUpdates[$oid]);
2362 216
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2363 216
    }
2364
2365
    /**
2366
     * Checks whether a PersistentCollection is scheduled for update.
2367
     *
2368
     * @internal
2369
     */
2370 140
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2371
    {
2372 140
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2373
    }
2374
2375
    /**
2376
     * Gets PersistentCollections that have been visited during computing change
2377
     * set of $document
2378
     *
2379
     * @internal
2380
     *
2381
     * @return PersistentCollectionInterface[]
2382
     */
2383 584
    public function getVisitedCollections(object $document) : array
2384
    {
2385 584
        $oid = spl_object_hash($document);
2386
2387 584
        return $this->visitedCollections[$oid] ?? [];
2388
    }
2389
2390
    /**
2391
     * Gets PersistentCollections that are scheduled to update and related to $document
2392
     *
2393
     * @internal
2394
     *
2395
     * @return PersistentCollectionInterface[]
2396
     */
2397 584
    public function getScheduledCollections(object $document) : array
2398
    {
2399 584
        $oid = spl_object_hash($document);
2400
2401 584
        return $this->hasScheduledCollections[$oid] ?? [];
2402
    }
2403
2404
    /**
2405
     * Checks whether the document is related to a PersistentCollection
2406
     * scheduled for update or deletion.
2407
     *
2408
     * @internal
2409
     */
2410 57
    public function hasScheduledCollections(object $document) : bool
2411
    {
2412 57
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2413
    }
2414
2415
    /**
2416
     * Marks the PersistentCollection's top-level owner as having a relation to
2417
     * a collection scheduled for update or deletion.
2418
     *
2419
     * If the owner is not scheduled for any lifecycle action, it will be
2420
     * scheduled for update to ensure that versioning takes place if necessary.
2421
     *
2422
     * If the collection is nested within atomic collection, it is immediately
2423
     * unscheduled and atomic one is scheduled for update instead. This makes
2424
     * calculating update data way easier.
2425
     */
2426 256
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2427
    {
2428 256
        if ($coll->getOwner() === null) {
2429
            return;
2430
        }
2431
2432 256
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2433 256
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2434
2435 256
        if ($document !== $coll->getOwner()) {
2436 26
            $parent  = $coll->getOwner();
2437 26
            $mapping = [];
2438 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2439 26
                [$mapping, $parent ] = $parentAssoc;
2440
            }
2441 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2442 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2443 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2444 8
                $this->scheduleCollectionUpdate($atomicCollection);
2445 8
                $this->unscheduleCollectionDeletion($coll);
2446 8
                $this->unscheduleCollectionUpdate($coll);
2447
            }
2448
        }
2449
2450 256
        if ($this->isDocumentScheduled($document)) {
2451 251
            return;
2452
        }
2453
2454 55
        $this->scheduleForUpdate($document);
2455 55
    }
2456
2457
    /**
2458
     * Get the top-most owning document of a given document
2459
     *
2460
     * If a top-level document is provided, that same document will be returned.
2461
     * For an embedded document, we will walk through parent associations until
2462
     * we find a top-level document.
2463
     *
2464
     * @throws UnexpectedValueException When a top-level document could not be found.
2465
     */
2466 258
    public function getOwningDocument(object $document) : object
2467
    {
2468 258
        $class = $this->dm->getClassMetadata(get_class($document));
2469 258
        while ($class->isEmbeddedDocument) {
2470 42
            $parentAssociation = $this->getParentAssociation($document);
2471
2472 42
            if (! $parentAssociation) {
2473
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2474
            }
2475
2476 42
            [, $document ] = $parentAssociation;
2477 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2478
        }
2479
2480 258
        return $document;
2481
    }
2482
2483
    /**
2484
     * Gets the class name for an association (embed or reference) with respect
2485
     * to any discriminator value.
2486
     *
2487
     * @internal
2488
     *
2489
     * @param array|null $data
2490
     */
2491 228
    public function getClassNameForAssociation(array $mapping, $data) : string
2492
    {
2493 228
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2494
2495 228
        $discriminatorValue = null;
2496 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2497 13
            $discriminatorValue = $data[$discriminatorField];
2498 216
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2499
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2500
        }
2501
2502 228
        if ($discriminatorValue !== null) {
2503 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2504 13
                ?? (string) $discriminatorValue;
2505
        }
2506
2507 216
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2508
2509 216
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2510 11
            $discriminatorValue = $data[$class->discriminatorField];
2511 206
        } elseif ($class->defaultDiscriminatorValue !== null) {
2512 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2513
        }
2514
2515 216
        if ($discriminatorValue !== null) {
2516 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2517
        }
2518
2519 205
        return $mapping['targetDocument'];
2520
    }
2521
2522
    /**
2523
     * Creates a document. Used for reconstitution of documents during hydration.
2524
     *
2525
     * @internal
2526
     */
2527 403
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2528
    {
2529 403
        $class = $this->dm->getClassMetadata($className);
2530
2531
        // @TODO figure out how to remove this
2532 403
        $discriminatorValue = null;
2533 403
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2534 15
            $discriminatorValue = $data[$class->discriminatorField];
2535 395
        } elseif (isset($class->defaultDiscriminatorValue)) {
2536 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2537
        }
2538
2539 403
        if ($discriminatorValue !== null) {
2540 16
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2541
2542 16
            $class = $this->dm->getClassMetadata($className);
2543
2544 16
            unset($data[$class->discriminatorField]);
2545
        }
2546
2547 403
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2548 2
            $document = $class->newInstance();
2549 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2550 2
            return $document;
2551
        }
2552
2553 402
        $isManagedObject = false;
2554 402
        $serializedId    = null;
2555 402
        $id              = null;
2556 402
        if (! $class->isQueryResultDocument) {
2557 399
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2558 399
            $serializedId    = serialize($id);
2559 399
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2560
        }
2561
2562 402
        $oid = null;
2563 402
        if ($isManagedObject) {
2564 101
            $document = $this->identityMap[$class->name][$serializedId];
2565 101
            $oid      = spl_object_hash($document);
2566 101
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
2567 16
                $document->setProxyInitializer(null);
2568 16
                $overrideLocalValues = true;
2569 16
                if ($document instanceof NotifyPropertyChanged) {
2570 16
                    $document->addPropertyChangedListener($this);
2571
                }
2572
            } else {
2573 91
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2574
            }
2575 101
            if ($overrideLocalValues) {
2576 42
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2577 101
                $this->originalDocumentData[$oid] = $data;
2578
            }
2579
        } else {
2580 353
            if ($document === null) {
2581 353
                $document = $class->newInstance();
2582
            }
2583
2584 353
            if (! $class->isQueryResultDocument) {
2585 349
                $this->registerManaged($document, $id, $data);
2586 349
                $oid                                            = spl_object_hash($document);
2587 349
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2588 349
                $this->identityMap[$class->name][$serializedId] = $document;
2589
            }
2590
2591 353
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2592
2593 353
            if (! $class->isQueryResultDocument) {
2594 349
                $this->originalDocumentData[$oid] = $data;
2595
            }
2596
        }
2597
2598 402
        return $document;
2599
    }
2600
2601
    /**
2602
     * Initializes (loads) an uninitialized persistent collection of a document.
2603
     *
2604
     * @internal
2605
     */
2606 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
2607
    {
2608 178
        if ($collection->getOwner() === null) {
2609
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2610
        }
2611
2612 178
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2613 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2614 178
    }
2615
2616
    /**
2617
     * Gets the identity map of the UnitOfWork.
2618
     *
2619
     * @internal
2620
     */
2621
    public function getIdentityMap() : array
2622
    {
2623
        return $this->identityMap;
2624
    }
2625
2626
    /**
2627
     * Gets the original data of a document. The original data is the data that was
2628
     * present at the time the document was reconstituted from the database.
2629
     *
2630
     * @return array
2631
     */
2632 1
    public function getOriginalDocumentData(object $document) : array
2633
    {
2634 1
        $oid = spl_object_hash($document);
2635
2636 1
        return $this->originalDocumentData[$oid] ?? [];
2637
    }
2638
2639
    /**
2640
     * @internal
2641
     */
2642 60
    public function setOriginalDocumentData(object $document, array $data) : void
2643
    {
2644 60
        $oid                              = spl_object_hash($document);
2645 60
        $this->originalDocumentData[$oid] = $data;
2646 60
        unset($this->documentChangeSets[$oid]);
2647 60
    }
2648
2649
    /**
2650
     * Sets a property value of the original data array of a document.
2651
     *
2652
     * @internal
2653
     *
2654
     * @param mixed $value
2655
     */
2656 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2657
    {
2658 3
        $this->originalDocumentData[$oid][$property] = $value;
2659 3
    }
2660
2661
    /**
2662
     * Gets the identifier of a document.
2663
     *
2664
     * @return mixed The identifier value
2665
     */
2666 454
    public function getDocumentIdentifier(object $document)
2667
    {
2668 454
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2669
    }
2670
2671
    /**
2672
     * Checks whether the UnitOfWork has any pending insertions.
2673
     *
2674
     * @internal
2675
     *
2676
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2677
     */
2678
    public function hasPendingInsertions() : bool
2679
    {
2680
        return ! empty($this->documentInsertions);
2681
    }
2682
2683
    /**
2684
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2685
     * number of documents in the identity map.
2686
     *
2687
     * @internal
2688
     */
2689 2
    public function size() : int
2690
    {
2691 2
        $count = 0;
2692 2
        foreach ($this->identityMap as $documentSet) {
2693 2
            $count += count($documentSet);
2694
        }
2695 2
        return $count;
2696
    }
2697
2698
    /**
2699
     * Registers a document as managed.
2700
     *
2701
     * TODO: This method assumes that $id is a valid PHP identifier for the
2702
     * document class. If the class expects its database identifier to be an
2703
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2704
     * document identifiers map will become inconsistent with the identity map.
2705
     * In the future, we may want to round-trip $id through a PHP and database
2706
     * conversion and throw an exception if it's inconsistent.
2707
     *
2708
     * @internal
2709
     *
2710
     * @param mixed $id The identifier values.
2711
     */
2712 382
    public function registerManaged(object $document, $id, array $data) : void
2713
    {
2714 382
        $oid   = spl_object_hash($document);
2715 382
        $class = $this->dm->getClassMetadata(get_class($document));
2716
2717 382
        if (! $class->identifier || $id === null) {
2718 110
            $this->documentIdentifiers[$oid] = $oid;
2719
        } else {
2720 376
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2721
        }
2722
2723 382
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2724 382
        $this->originalDocumentData[$oid] = $data;
2725 382
        $this->addToIdentityMap($document);
2726 382
    }
2727
2728
    /**
2729
     * Clears the property changeset of the document with the given OID.
2730
     *
2731
     * @internal
2732
     */
2733
    public function clearDocumentChangeSet(string $oid)
2734
    {
2735
        $this->documentChangeSets[$oid] = [];
2736
    }
2737
2738
    /* PropertyChangedListener implementation */
2739
2740
    /**
2741
     * Notifies this UnitOfWork of a property change in a document.
2742
     *
2743
     * @param object $document     The document that owns the property.
2744
     * @param string $propertyName The name of the property that changed.
2745
     * @param mixed  $oldValue     The old value of the property.
2746
     * @param mixed  $newValue     The new value of the property.
2747
     */
2748 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2749
    {
2750 2
        $oid   = spl_object_hash($document);
2751 2
        $class = $this->dm->getClassMetadata(get_class($document));
2752
2753 2
        if (! isset($class->fieldMappings[$propertyName])) {
2754 1
            return; // ignore non-persistent fields
2755
        }
2756
2757
        // Update changeset and mark document for synchronization
2758 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2759 2
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2760
            return;
2761
        }
2762
2763 2
        $this->scheduleForSynchronization($document);
2764 2
    }
2765
2766
    /**
2767
     * Gets the currently scheduled document insertions in this UnitOfWork.
2768
     */
2769 3
    public function getScheduledDocumentInsertions() : array
2770
    {
2771 3
        return $this->documentInsertions;
2772
    }
2773
2774
    /**
2775
     * Gets the currently scheduled document upserts in this UnitOfWork.
2776
     */
2777 1
    public function getScheduledDocumentUpserts() : array
2778
    {
2779 1
        return $this->documentUpserts;
2780
    }
2781
2782
    /**
2783
     * Gets the currently scheduled document updates in this UnitOfWork.
2784
     */
2785 2
    public function getScheduledDocumentUpdates() : array
2786
    {
2787 2
        return $this->documentUpdates;
2788
    }
2789
2790
    /**
2791
     * Gets the currently scheduled document deletions in this UnitOfWork.
2792
     */
2793
    public function getScheduledDocumentDeletions() : array
2794
    {
2795
        return $this->documentDeletions;
2796
    }
2797
2798
    /**
2799
     * Get the currently scheduled complete collection deletions
2800
     *
2801
     * @internal
2802
     */
2803
    public function getScheduledCollectionDeletions() : array
2804
    {
2805
        return $this->collectionDeletions;
2806
    }
2807
2808
    /**
2809
     * Gets the currently scheduled collection inserts, updates and deletes.
2810
     *
2811
     * @internal
2812
     */
2813
    public function getScheduledCollectionUpdates() : array
2814
    {
2815
        return $this->collectionUpdates;
2816
    }
2817
2818
    /**
2819
     * Helper method to initialize a lazy loading proxy or persistent collection.
2820
     *
2821
     * @internal
2822
     */
2823
    public function initializeObject(object $obj) : void
2824
    {
2825
        if ($obj instanceof GhostObjectInterface) {
2826
            $obj->initializeProxy();
2827
        } elseif ($obj instanceof PersistentCollectionInterface) {
2828
            $obj->initialize();
2829
        }
2830
    }
2831
2832
    private function objToStr(object $obj) : string
2833
    {
2834
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2835
    }
2836
}
2837