Completed
Push — master ( a8fe50...bce26f )
by Maciej
13s
created

lib/Doctrine/ODM/MongoDB/UnitOfWork.php (7 issues)

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\PersistentCollectionInterface;
16
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
17
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
18
use Doctrine\ODM\MongoDB\Proxy\Proxy;
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 MongoDB\BSON\UTCDateTime;
25
use function array_filter;
26
use function count;
27
use function get_class;
28
use function in_array;
29
use function is_array;
30
use function is_object;
31
use function method_exists;
32
use function preg_match;
33
use function serialize;
34
use function spl_object_hash;
35
use function sprintf;
36
37
/**
38
 * The UnitOfWork is responsible for tracking changes to objects during an
39
 * "object-level" transaction and for writing out changes to the database
40
 * in the correct order.
41
 *
42
 */
43
class UnitOfWork implements PropertyChangedListener
44
{
45
    /**
46
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
47
     */
48
    public const STATE_MANAGED = 1;
49
50
    /**
51
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
52
     * and is not (yet) managed by a DocumentManager.
53
     */
54
    public const STATE_NEW = 2;
55
56
    /**
57
     * A detached document is an instance with a persistent identity that is not
58
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
59
     */
60
    public const STATE_DETACHED = 3;
61
62
    /**
63
     * A removed document instance is an instance with a persistent identity,
64
     * associated with a DocumentManager, whose persistent state has been
65
     * deleted (or is scheduled for deletion).
66
     */
67
    public const STATE_REMOVED = 4;
68
69
    /**
70
     * The identity map holds references to all managed documents.
71
     *
72
     * Documents are grouped by their class name, and then indexed by the
73
     * serialized string of their database identifier field or, if the class
74
     * has no identifier, the SPL object hash. Serializing the identifier allows
75
     * differentiation of values that may be equal (via type juggling) but not
76
     * identical.
77
     *
78
     * Since all classes in a hierarchy must share the same identifier set,
79
     * we always take the root class name of the hierarchy.
80
     *
81
     * @var array
82
     */
83
    private $identityMap = [];
84
85
    /**
86
     * Map of all identifiers of managed documents.
87
     * Keys are object ids (spl_object_hash).
88
     *
89
     * @var array
90
     */
91
    private $documentIdentifiers = [];
92
93
    /**
94
     * Map of the original document data of managed documents.
95
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
96
     * at commit time.
97
     *
98
     * @var array
99
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
100
     *           A value will only really be copied if the value in the document is modified
101
     *           by the user.
102
     */
103
    private $originalDocumentData = [];
104
105
    /**
106
     * Map of document changes. Keys are object ids (spl_object_hash).
107
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
108
     *
109
     * @var array
110
     */
111
    private $documentChangeSets = [];
112
113
    /**
114
     * The (cached) states of any known documents.
115
     * Keys are object ids (spl_object_hash).
116
     *
117
     * @var array
118
     */
119
    private $documentStates = [];
120
121
    /**
122
     * Map of documents that are scheduled for dirty checking at commit time.
123
     *
124
     * Documents are grouped by their class name, and then indexed by their SPL
125
     * object hash. This is only used for documents with a change tracking
126
     * policy of DEFERRED_EXPLICIT.
127
     *
128
     * @var array
129
     * @todo rename: scheduledForSynchronization
130
     */
131
    private $scheduledForDirtyCheck = [];
132
133
    /**
134
     * A list of all pending document insertions.
135
     *
136
     * @var array
137
     */
138
    private $documentInsertions = [];
139
140
    /**
141
     * A list of all pending document updates.
142
     *
143
     * @var array
144
     */
145
    private $documentUpdates = [];
146
147
    /**
148
     * A list of all pending document upserts.
149
     *
150
     * @var array
151
     */
152
    private $documentUpserts = [];
153
154
    /**
155
     * A list of all pending document deletions.
156
     *
157
     * @var array
158
     */
159
    private $documentDeletions = [];
160
161
    /**
162
     * All pending collection deletions.
163
     *
164
     * @var array
165
     */
166
    private $collectionDeletions = [];
167
168
    /**
169
     * All pending collection updates.
170
     *
171
     * @var array
172
     */
173
    private $collectionUpdates = [];
174
175
    /**
176
     * A list of documents related to collections scheduled for update or deletion
177
     *
178
     * @var array
179
     */
180
    private $hasScheduledCollections = [];
181
182
    /**
183
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
184
     * At the end of the UnitOfWork all these collections will make new snapshots
185
     * of their data.
186
     *
187
     * @var array
188
     */
189
    private $visitedCollections = [];
190
191
    /**
192
     * The DocumentManager that "owns" this UnitOfWork instance.
193
     *
194
     * @var DocumentManager
195
     */
196
    private $dm;
197
198
    /**
199
     * The EventManager used for dispatching events.
200
     *
201
     * @var EventManager
202
     */
203
    private $evm;
204
205
    /**
206
     * Additional documents that are scheduled for removal.
207
     *
208
     * @var array
209
     */
210
    private $orphanRemovals = [];
211
212
    /**
213
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
214
     *
215
     * @var HydratorFactory
216
     */
217
    private $hydratorFactory;
218
219
    /**
220
     * The document persister instances used to persist document instances.
221
     *
222
     * @var array
223
     */
224
    private $persisters = [];
225
226
    /**
227
     * The collection persister instance used to persist changes to collections.
228
     *
229
     * @var Persisters\CollectionPersister
230
     */
231
    private $collectionPersister;
232
233
    /**
234
     * The persistence builder instance used in DocumentPersisters.
235
     *
236
     * @var PersistenceBuilder
237
     */
238
    private $persistenceBuilder;
239
240
    /**
241
     * Array of parent associations between embedded documents.
242
     *
243
     * @var array
244
     */
245
    private $parentAssociations = [];
246
247
    /** @var LifecycleEventManager */
248
    private $lifecycleEventManager;
249
250
    /**
251
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
252
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
253
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
254
     *
255
     * @var array
256
     */
257
    private $embeddedDocumentsRegistry = [];
258
259
    /** @var int */
260
    private $commitsInProgress = 0;
261
262
    /**
263
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
264
     *
265
     */
266 1633
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
267
    {
268 1633
        $this->dm = $dm;
269 1633
        $this->evm = $evm;
270 1633
        $this->hydratorFactory = $hydratorFactory;
271 1633
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
272 1633
    }
273
274
    /**
275
     * Factory for returning new PersistenceBuilder instances used for preparing data into
276
     * queries for insert persistence.
277
     */
278 1123
    public function getPersistenceBuilder(): PersistenceBuilder
279
    {
280 1123
        if (! $this->persistenceBuilder) {
281 1123
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
282
        }
283 1123
        return $this->persistenceBuilder;
284
    }
285
286
    /**
287
     * Sets the parent association for a given embedded document.
288
     */
289 202
    public function setParentAssociation(object $document, array $mapping, object $parent, string $propertyPath): void
290
    {
291 202
        $oid = spl_object_hash($document);
292 202
        $this->embeddedDocumentsRegistry[$oid] = $document;
293 202
        $this->parentAssociations[$oid] = [$mapping, $parent, $propertyPath];
294 202
    }
295
296
    /**
297
     * Gets the parent association for a given embedded document.
298
     *
299
     *     <code>
300
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
301
     *     </code>
302
     */
303 226
    public function getParentAssociation(object $document): ?array
304
    {
305 226
        $oid = spl_object_hash($document);
306
307 226
        return $this->parentAssociations[$oid] ?? null;
308
    }
309
310
    /**
311
     * Get the document persister instance for the given document name
312
     */
313 1121
    public function getDocumentPersister(string $documentName): Persisters\DocumentPersister
314
    {
315 1121
        if (! isset($this->persisters[$documentName])) {
316 1108
            $class = $this->dm->getClassMetadata($documentName);
317 1108
            $pb = $this->getPersistenceBuilder();
318 1108
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
319
        }
320 1121
        return $this->persisters[$documentName];
321
    }
322
323
    /**
324
     * Get the collection persister instance.
325
     */
326 1121
    public function getCollectionPersister(): CollectionPersister
327
    {
328 1121
        if (! isset($this->collectionPersister)) {
329 1121
            $pb = $this->getPersistenceBuilder();
330 1121
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
331
        }
332 1121
        return $this->collectionPersister;
333
    }
334
335
    /**
336
     * Set the document persister instance to use for the given document name
337
     */
338 13
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister): void
339
    {
340 13
        $this->persisters[$documentName] = $persister;
341 13
    }
342
343
    /**
344
     * Commits the UnitOfWork, executing all operations that have been postponed
345
     * up to this point. The state of all managed documents will be synchronized with
346
     * the database.
347
     *
348
     * The operations are executed in the following order:
349
     *
350
     * 1) All document insertions
351
     * 2) All document updates
352
     * 3) All document deletions
353
     *
354
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
355
     */
356 603
    public function commit(array $options = []): void
357
    {
358
        // Raise preFlush
359 603
        if ($this->evm->hasListeners(Events::preFlush)) {
360
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
361
        }
362
363
        // Compute changes done since last commit.
364 603
        $this->computeChangeSets();
365
366 602
        if (! ($this->documentInsertions ||
367 255
            $this->documentUpserts ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
368 213
            $this->documentDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
369 196
            $this->documentUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
370 22
            $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
371 22
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
372 602
            $this->orphanRemovals)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
373
        ) {
374 22
            return; // Nothing to do.
375
        }
376
377 599
        $this->commitsInProgress++;
378 599
        if ($this->commitsInProgress > 1) {
379
            throw MongoDBException::commitInProgress();
380
        }
381
        try {
382 599
            if ($this->orphanRemovals) {
383 50
                foreach ($this->orphanRemovals as $removal) {
384 50
                    $this->remove($removal);
385
                }
386
            }
387
388
            // Raise onFlush
389 599
            if ($this->evm->hasListeners(Events::onFlush)) {
390 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
391
            }
392
393 598
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
394 86
                list($class, $documents) = $classAndDocuments;
395 86
                $this->executeUpserts($class, $documents, $options);
396
            }
397
398 598
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
399 522
                list($class, $documents) = $classAndDocuments;
400 522
                $this->executeInserts($class, $documents, $options);
401
            }
402
403 597
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
404 228
                list($class, $documents) = $classAndDocuments;
405 228
                $this->executeUpdates($class, $documents, $options);
406
            }
407
408 597
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
409 74
                list($class, $documents) = $classAndDocuments;
410 74
                $this->executeDeletions($class, $documents, $options);
411
            }
412
413
            // Raise postFlush
414 597
            if ($this->evm->hasListeners(Events::postFlush)) {
415
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
416
            }
417
418
            // Clear up
419 597
            $this->documentInsertions =
420 597
            $this->documentUpserts =
421 597
            $this->documentUpdates =
422 597
            $this->documentDeletions =
423 597
            $this->documentChangeSets =
424 597
            $this->collectionUpdates =
425 597
            $this->collectionDeletions =
426 597
            $this->visitedCollections =
427 597
            $this->scheduledForDirtyCheck =
428 597
            $this->orphanRemovals =
429 597
            $this->hasScheduledCollections = [];
430 597
        } finally {
431 599
            $this->commitsInProgress--;
432
        }
433 597
    }
434
435
    /**
436
     * Groups a list of scheduled documents by their class.
437
     */
438 598
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false): array
439
    {
440 598
        if (empty($documents)) {
441 598
            return [];
442
        }
443 597
        $divided = [];
444 597
        $embeds = [];
445 597
        foreach ($documents as $oid => $d) {
446 597
            $className = get_class($d);
447 597
            if (isset($embeds[$className])) {
448 74
                continue;
449
            }
450 597
            if (isset($divided[$className])) {
451 159
                $divided[$className][1][$oid] = $d;
452 159
                continue;
453
            }
454 597
            $class = $this->dm->getClassMetadata($className);
455 597
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
456 176
                $embeds[$className] = true;
457 176
                continue;
458
            }
459 597
            if (empty($divided[$class->name])) {
460 597
                $divided[$class->name] = [$class, [$oid => $d]];
461
            } else {
462 597
                $divided[$class->name][1][$oid] = $d;
463
            }
464
        }
465 597
        return $divided;
466
    }
467
468
    /**
469
     * Compute changesets of all documents scheduled for insertion.
470
     *
471
     * Embedded documents will not be processed.
472
     */
473 608
    private function computeScheduleInsertsChangeSets(): void
474
    {
475 608
        foreach ($this->documentInsertions as $document) {
476 535
            $class = $this->dm->getClassMetadata(get_class($document));
477 535
            if ($class->isEmbeddedDocument) {
478 158
                continue;
479
            }
480
481 529
            $this->computeChangeSet($class, $document);
482
        }
483 607
    }
484
485
    /**
486
     * Compute changesets of all documents scheduled for upsert.
487
     *
488
     * Embedded documents will not be processed.
489
     */
490 607
    private function computeScheduleUpsertsChangeSets(): void
491
    {
492 607
        foreach ($this->documentUpserts as $document) {
493 85
            $class = $this->dm->getClassMetadata(get_class($document));
494 85
            if ($class->isEmbeddedDocument) {
495
                continue;
496
            }
497
498 85
            $this->computeChangeSet($class, $document);
499
        }
500 607
    }
501
502
    /**
503
     * Gets the changeset for a document.
504
     *
505
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
506
     */
507 603
    public function getDocumentChangeSet(object $document): array
508
    {
509 603
        $oid = spl_object_hash($document);
510
511 603
        return $this->documentChangeSets[$oid] ?? [];
512
    }
513
514
    /**
515
     * INTERNAL:
516
     * Sets the changeset for a document.
517
     */
518 1
    public function setDocumentChangeSet(object $document, array $changeset): void
519
    {
520 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
521 1
    }
522
523
    /**
524
     * Get a documents actual data, flattening all the objects to arrays.
525
     *
526
     * @return array
527
     */
528 608
    public function getDocumentActualData(object $document): array
529
    {
530 608
        $class = $this->dm->getClassMetadata(get_class($document));
531 608
        $actualData = [];
532 608
        foreach ($class->reflFields as $name => $refProp) {
533 608
            $mapping = $class->fieldMappings[$name];
534
            // skip not saved fields
535 608
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
536 48
                continue;
537
            }
538 608
            $value = $refProp->getValue($document);
539 608
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
540 608
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
541
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
542 394
                if (! $value instanceof Collection) {
543 146
                    $value = new ArrayCollection($value);
544
                }
545
546
                // Inject PersistentCollection
547 394
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
548 394
                $coll->setOwner($document, $mapping);
549 394
                $coll->setDirty(! $value->isEmpty());
550 394
                $class->reflFields[$name]->setValue($document, $coll);
551 394
                $actualData[$name] = $coll;
552
            } else {
553 608
                $actualData[$name] = $value;
554
            }
555
        }
556 608
        return $actualData;
557
    }
558
559
    /**
560
     * Computes the changes that happened to a single document.
561
     *
562
     * Modifies/populates the following properties:
563
     *
564
     * {@link originalDocumentData}
565
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
566
     * then it was not fetched from the database and therefore we have no original
567
     * document data yet. All of the current document data is stored as the original document data.
568
     *
569
     * {@link documentChangeSets}
570
     * The changes detected on all properties of the document are stored there.
571
     * A change is a tuple array where the first entry is the old value and the second
572
     * entry is the new value of the property. Changesets are used by persisters
573
     * to INSERT/UPDATE the persistent document state.
574
     *
575
     * {@link documentUpdates}
576
     * If the document is already fully MANAGED (has been fetched from the database before)
577
     * and any changes to its properties are detected, then a reference to the document is stored
578
     * there to mark it for an update.
579
     */
580 604
    public function computeChangeSet(ClassMetadata $class, object $document): void
581
    {
582 604
        if (! $class->isInheritanceTypeNone()) {
583 183
            $class = $this->dm->getClassMetadata(get_class($document));
584
        }
585
586
        // Fire PreFlush lifecycle callbacks
587 604
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
588 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
589
        }
590
591 604
        $this->computeOrRecomputeChangeSet($class, $document);
592 603
    }
593
594
    /**
595
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
596
     */
597 604
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false): void
598
    {
599 604
        $oid = spl_object_hash($document);
600 604
        $actualData = $this->getDocumentActualData($document);
601 604
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
602 604
        if ($isNewDocument) {
603
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
604
            // These result in an INSERT.
605 603
            $this->originalDocumentData[$oid] = $actualData;
606 603
            $changeSet = [];
607 603
            foreach ($actualData as $propName => $actualValue) {
608
                /* At this PersistentCollection shouldn't be here, probably it
609
                 * was cloned and its ownership must be fixed
610
                 */
611 603
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
612
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
613
                    $actualValue = $actualData[$propName];
614
                }
615
                // ignore inverse side of reference relationship
616 603
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
617 188
                    continue;
618
                }
619 603
                $changeSet[$propName] = [null, $actualValue];
620
            }
621 603
            $this->documentChangeSets[$oid] = $changeSet;
622
        } else {
623 288
            if ($class->isReadOnly) {
624 2
                return;
625
            }
626
            // Document is "fully" MANAGED: it was already fully persisted before
627
            // and we have a copy of the original data
628 286
            $originalData = $this->originalDocumentData[$oid];
629 286
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
630 286
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
631 2
                $changeSet = $this->documentChangeSets[$oid];
632
            } else {
633 286
                $changeSet = [];
634
            }
635
636 286
            $gridFSMetadataProperty = null;
637
638 286
            if ($class->isFile) {
639
                try {
640 3
                    $gridFSMetadata = $class->getFieldMappingByDbFieldName('metadata');
641 2
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
642 1
                } catch (MappingException $e) {
643
                }
644
            }
645
646 286
            foreach ($actualData as $propName => $actualValue) {
647
                // skip not saved fields
648 286
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
649 286
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
650 3
                    continue;
651
                }
652
653 285
                $orgValue = $originalData[$propName] ?? null;
654
655
                // skip if value has not changed
656 285
                if ($orgValue === $actualValue) {
657 283
                    if (! $actualValue instanceof PersistentCollectionInterface) {
658 283
                        continue;
659
                    }
660
661 199
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
662
                        // consider dirty collections as changed as well
663 175
                        continue;
664
                    }
665
                }
666
667
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
668 246
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
669 14
                    if ($orgValue !== null) {
670 8
                        $this->scheduleOrphanRemoval($orgValue);
671
                    }
672 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
673 14
                    continue;
674
                }
675
676
                // if owning side of reference-one relationship
677 239
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
678 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
679 1
                        $this->scheduleOrphanRemoval($orgValue);
680
                    }
681
682 13
                    $changeSet[$propName] = [$orgValue, $actualValue];
683 13
                    continue;
684
                }
685
686 232
                if ($isChangeTrackingNotify) {
687 3
                    continue;
688
                }
689
690
                // ignore inverse side of reference relationship
691 230
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
692 6
                    continue;
693
                }
694
695
                // Persistent collection was exchanged with the "originally"
696
                // created one. This can only mean it was cloned and replaced
697
                // on another document.
698 228
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
699 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
700
                }
701
702
                // if embed-many or reference-many relationship
703 228
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
704 119
                    $changeSet[$propName] = [$orgValue, $actualValue];
705
                    /* If original collection was exchanged with a non-empty value
706
                     * and $set will be issued, there is no need to $unset it first
707
                     */
708 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
709 27
                        continue;
710
                    }
711 100
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
712 18
                        $this->scheduleCollectionDeletion($orgValue);
713
                    }
714 100
                    continue;
715
                }
716
717
                // skip equivalent date values
718 146
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
719
                    /** @var DateType $dateType */
720 37
                    $dateType = Type::getType('date');
721 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
722 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
723
724 37
                    $orgTimestamp = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null;
725 37
                    $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null;
726
727 37
                    if ($orgTimestamp === $actualTimestamp) {
728 30
                        continue;
729
                    }
730
                }
731
732
                // regular field
733 129
                $changeSet[$propName] = [$orgValue, $actualValue];
734
            }
735 286
            if ($changeSet) {
736 235
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
737 19
                    ? $changeSet + $this->documentChangeSets[$oid]
738 232
                    : $changeSet;
739
740 235
                $this->originalDocumentData[$oid] = $actualData;
741 235
                $this->scheduleForUpdate($document);
742
            }
743
        }
744
745
        // Look for changes in associations of the document
746 604
        $associationMappings = array_filter(
747 604
            $class->associationMappings,
748
            function ($assoc) {
749 465
                return empty($assoc['notSaved']);
750 604
            }
751
        );
752
753 604
        foreach ($associationMappings as $mapping) {
754 465
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
755
756 465
            if ($value === null) {
757 323
                continue;
758
            }
759
760 446
            $this->computeAssociationChanges($document, $mapping, $value);
761
762 445
            if (isset($mapping['reference'])) {
763 337
                continue;
764
            }
765
766 347
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
767
768 347
            foreach ($values as $obj) {
769 181
                $oid2 = spl_object_hash($obj);
770
771 181
                if (isset($this->documentChangeSets[$oid2])) {
772 179
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
773
                        // instance of $value is the same as it was previously otherwise there would be
774
                        // change set already in place
775 41
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
776
                    }
777
778 179
                    if (! $isNewDocument) {
779 81
                        $this->scheduleForUpdate($document);
780
                    }
781
782 347
                    break;
783
                }
784
            }
785
        }
786 603
    }
787
788
    /**
789
     * Computes all the changes that have been done to documents and collections
790
     * since the last commit and stores these changes in the _documentChangeSet map
791
     * temporarily for access by the persisters, until the UoW commit is finished.
792
     */
793 608
    public function computeChangeSets(): void
794
    {
795 608
        $this->computeScheduleInsertsChangeSets();
796 607
        $this->computeScheduleUpsertsChangeSets();
797
798
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
799 607
        foreach ($this->identityMap as $className => $documents) {
800 607
            $class = $this->dm->getClassMetadata($className);
801 607
            if ($class->isEmbeddedDocument) {
802
                /* we do not want to compute changes to embedded documents up front
803
                 * in case embedded document was replaced and its changeset
804
                 * would corrupt data. Embedded documents' change set will
805
                 * be calculated by reachability from owning document.
806
                 */
807 171
                continue;
808
            }
809
810
            // If change tracking is explicit or happens through notification, then only compute
811
            // changes on document of that type that are explicitly marked for synchronization.
812
            switch (true) {
813 607
                case ($class->isChangeTrackingDeferredImplicit()):
814 606
                    $documentsToProcess = $documents;
815 606
                    break;
816
817 4
                case (isset($this->scheduledForDirtyCheck[$className])):
818 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
819 3
                    break;
820
821
                default:
822 4
                    $documentsToProcess = [];
823
            }
824
825 607
            foreach ($documentsToProcess as $document) {
826
                // Ignore uninitialized proxy objects
827 602
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
828 10
                    continue;
829
                }
830
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
831 602
                $oid = spl_object_hash($document);
832 602
                if (isset($this->documentInsertions[$oid])
833 329
                    || isset($this->documentUpserts[$oid])
834 283
                    || isset($this->documentDeletions[$oid])
835 602
                    || ! isset($this->documentStates[$oid])
836
                ) {
837 600
                    continue;
838
                }
839
840 607
                $this->computeChangeSet($class, $document);
841
            }
842
        }
843 607
    }
844
845
    /**
846
     * Computes the changes of an association.
847
     *
848
     * @param mixed $value The value of the association.
849
     * @throws \InvalidArgumentException
850
     */
851 446
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value): void
852
    {
853 446
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
854 446
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
855 446
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
856
857 446
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
858 7
            return;
859
        }
860
861 445
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
862 251
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
863 247
                $this->scheduleCollectionUpdate($value);
864
            }
865 251
            $topmostOwner = $this->getOwningDocument($value->getOwner());
866 251
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
867 251
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
868 144
                $value->initialize();
869 144
                foreach ($value->getDeletedDocuments() as $orphan) {
870 23
                    $this->scheduleOrphanRemoval($orphan);
871
                }
872
            }
873
        }
874
875
        // Look through the documents, and in any of their associations,
876
        // for transient (new) documents, recursively. ("Persistence by reachability")
877
        // Unwrap. Uninitialized collections will simply be empty.
878 445
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap();
879
880 445
        $count = 0;
881 445
        foreach ($unwrappedValue as $key => $entry) {
882 361
            if (! is_object($entry)) {
883 1
                throw new \InvalidArgumentException(
884 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
885
                );
886
            }
887
888 360
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
889
890 360
            $state = $this->getDocumentState($entry, self::STATE_NEW);
891
892
            // Handle "set" strategy for multi-level hierarchy
893 360
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
894 360
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
895
896 360
            $count++;
897
898
            switch ($state) {
899 360
                case self::STATE_NEW:
900 67
                    if (! $assoc['isCascadePersist']) {
901
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
902
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
903
                            . ' Explicitly persist the new document or configure cascading persist operations'
904
                            . ' on the relationship.');
905
                    }
906
907 67
                    $this->persistNew($targetClass, $entry);
908 67
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
909 67
                    $this->computeChangeSet($targetClass, $entry);
910 67
                    break;
911
912 356
                case self::STATE_MANAGED:
913 356
                    if ($targetClass->isEmbeddedDocument) {
914 172
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
915 172
                        if ($knownParent && $knownParent !== $parentDocument) {
916 6
                            $entry = clone $entry;
917 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
918 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
919 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
920 3
                                $poid = spl_object_hash($parentDocument);
921 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
922 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
923
                                }
924
                            } else {
925
                                // must use unwrapped value to not trigger orphan removal
926 4
                                $unwrappedValue[$key] = $entry;
927
                            }
928 6
                            $this->persistNew($targetClass, $entry);
929
                        }
930 172
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
931 172
                        $this->computeChangeSet($targetClass, $entry);
932
                    }
933 356
                    break;
934
935 1
                case self::STATE_REMOVED:
936
                    // Consume the $value as array (it's either an array or an ArrayAccess)
937
                    // and remove the element from Collection.
938 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
939
                        unset($value[$key]);
940
                    }
941 1
                    break;
942
943
                case self::STATE_DETACHED:
944
                    // Can actually not happen right now as we assume STATE_NEW,
945
                    // so the exception will be raised from the DBAL layer (constraint violation).
946
                    throw new \InvalidArgumentException('A detached document was found through a '
947
                        . 'relationship during cascading a persist operation.');
948
949 360
                default:
950
                    // MANAGED associated documents are already taken into account
951
                    // during changeset calculation anyway, since they are in the identity map.
952
            }
953
        }
954 444
    }
955
956
    /**
957
     * INTERNAL:
958
     * Computes the changeset of an individual document, independently of the
959
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
960
     *
961
     * The passed document must be a managed document. If the document already has a change set
962
     * because this method is invoked during a commit cycle then the change sets are added.
963
     * whereby changes detected in this method prevail.
964
     *
965
     * @ignore
966
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
967
     */
968 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document): void
969
    {
970
        // Ignore uninitialized proxy objects
971 19
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
972 1
            return;
973
        }
974
975 18
        $oid = spl_object_hash($document);
976
977 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
978
            throw new \InvalidArgumentException('Document must be managed.');
979
        }
980
981 18
        if (! $class->isInheritanceTypeNone()) {
982 2
            $class = $this->dm->getClassMetadata(get_class($document));
983
        }
984
985 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
986 18
    }
987
988
    /**
989
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
990
     */
991 633
    private function persistNew(ClassMetadata $class, object $document): void
992
    {
993 633
        $this->lifecycleEventManager->prePersist($class, $document);
994 633
        $oid = spl_object_hash($document);
995 633
        $upsert = false;
996 633
        if ($class->identifier) {
997 633
            $idValue = $class->getIdentifierValue($document);
998 633
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
999
1000 633
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1001 3
                throw new \InvalidArgumentException(sprintf(
1002 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1003 3
                    get_class($document)
1004
                ));
1005
            }
1006
1007 632
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1008 1
                throw new \InvalidArgumentException(sprintf(
1009 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1010 1
                    get_class($document)
1011
                ));
1012
            }
1013
1014 631
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1015 552
                $idValue = $class->idGenerator->generate($this->dm, $document);
1016 552
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1017 552
                $class->setIdentifierValue($document, $idValue);
1018
            }
1019
1020 631
            $this->documentIdentifiers[$oid] = $idValue;
1021
        } else {
1022
            // this is for embedded documents without identifiers
1023 151
            $this->documentIdentifiers[$oid] = $oid;
1024
        }
1025
1026 631
        $this->documentStates[$oid] = self::STATE_MANAGED;
1027
1028 631
        if ($upsert) {
1029 89
            $this->scheduleForUpsert($class, $document);
1030
        } else {
1031 561
            $this->scheduleForInsert($class, $document);
1032
        }
1033 631
    }
1034
1035
    /**
1036
     * Executes all document insertions for documents of the specified type.
1037
     */
1038 522
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []): void
1039
    {
1040 522
        $persister = $this->getDocumentPersister($class->name);
1041
1042 522
        foreach ($documents as $oid => $document) {
1043 522
            $persister->addInsert($document);
1044 522
            unset($this->documentInsertions[$oid]);
1045
        }
1046
1047 522
        $persister->executeInserts($options);
1048
1049 521
        foreach ($documents as $document) {
1050 521
            $this->lifecycleEventManager->postPersist($class, $document);
1051
        }
1052 521
    }
1053
1054
    /**
1055
     * Executes all document upserts for documents of the specified type.
1056
     */
1057 86
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []): void
1058
    {
1059 86
        $persister = $this->getDocumentPersister($class->name);
1060
1061 86
        foreach ($documents as $oid => $document) {
1062 86
            $persister->addUpsert($document);
1063 86
            unset($this->documentUpserts[$oid]);
1064
        }
1065
1066 86
        $persister->executeUpserts($options);
1067
1068 86
        foreach ($documents as $document) {
1069 86
            $this->lifecycleEventManager->postPersist($class, $document);
1070
        }
1071 86
    }
1072
1073
    /**
1074
     * Executes all document updates for documents of the specified type.
1075
     */
1076 228
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []): void
1077
    {
1078 228
        if ($class->isReadOnly) {
1079
            return;
1080
        }
1081
1082 228
        $className = $class->name;
1083 228
        $persister = $this->getDocumentPersister($className);
1084
1085 228
        foreach ($documents as $oid => $document) {
1086 228
            $this->lifecycleEventManager->preUpdate($class, $document);
1087
1088 228
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1089 227
                $persister->update($document, $options);
1090
            }
1091
1092 222
            unset($this->documentUpdates[$oid]);
1093
1094 222
            $this->lifecycleEventManager->postUpdate($class, $document);
1095
        }
1096 221
    }
1097
1098
    /**
1099
     * Executes all document deletions for documents of the specified type.
1100
     */
1101 74
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []): void
1102
    {
1103 74
        $persister = $this->getDocumentPersister($class->name);
1104
1105 74
        foreach ($documents as $oid => $document) {
1106 74
            if (! $class->isEmbeddedDocument) {
1107 36
                $persister->delete($document, $options);
1108
            }
1109
            unset(
1110 72
                $this->documentDeletions[$oid],
1111 72
                $this->documentIdentifiers[$oid],
1112 72
                $this->originalDocumentData[$oid]
1113
            );
1114
1115
            // Clear snapshot information for any referenced PersistentCollection
1116
            // http://www.doctrine-project.org/jira/browse/MODM-95
1117 72
            foreach ($class->associationMappings as $fieldMapping) {
1118 48
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1119 38
                    continue;
1120
                }
1121
1122 28
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1123 28
                if (! ($value instanceof PersistentCollectionInterface)) {
1124 7
                    continue;
1125
                }
1126
1127 24
                $value->clearSnapshot();
1128
            }
1129
1130
            // Document with this $oid after deletion treated as NEW, even if the $oid
1131
            // is obtained by a new document because the old one went out of scope.
1132 72
            $this->documentStates[$oid] = self::STATE_NEW;
1133
1134 72
            $this->lifecycleEventManager->postRemove($class, $document);
1135
        }
1136 72
    }
1137
1138
    /**
1139
     * Schedules a document for insertion into the database.
1140
     * If the document already has an identifier, it will be added to the
1141
     * identity map.
1142
     *
1143
     * @throws \InvalidArgumentException
1144
     */
1145 564
    public function scheduleForInsert(ClassMetadata $class, object $document): void
1146
    {
1147 564
        $oid = spl_object_hash($document);
1148
1149 564
        if (isset($this->documentUpdates[$oid])) {
1150
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1151
        }
1152 564
        if (isset($this->documentDeletions[$oid])) {
1153
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1154
        }
1155 564
        if (isset($this->documentInsertions[$oid])) {
1156
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1157
        }
1158
1159 564
        $this->documentInsertions[$oid] = $document;
1160
1161 564
        if (! isset($this->documentIdentifiers[$oid])) {
1162 3
            return;
1163
        }
1164
1165 561
        $this->addToIdentityMap($document);
1166 561
    }
1167
1168
    /**
1169
     * Schedules a document for upsert into the database and adds it to the
1170
     * identity map
1171
     *
1172
     * @throws \InvalidArgumentException
1173
     */
1174 92
    public function scheduleForUpsert(ClassMetadata $class, object $document): void
1175
    {
1176 92
        $oid = spl_object_hash($document);
1177
1178 92
        if ($class->isEmbeddedDocument) {
1179
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1180
        }
1181 92
        if (isset($this->documentUpdates[$oid])) {
1182
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1183
        }
1184 92
        if (isset($this->documentDeletions[$oid])) {
1185
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1186
        }
1187 92
        if (isset($this->documentUpserts[$oid])) {
1188
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1189
        }
1190
1191 92
        $this->documentUpserts[$oid] = $document;
1192 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1193 92
        $this->addToIdentityMap($document);
1194 92
    }
1195
1196
    /**
1197
     * Checks whether a document is scheduled for insertion.
1198
     */
1199 104
    public function isScheduledForInsert(object $document): bool
1200
    {
1201 104
        return isset($this->documentInsertions[spl_object_hash($document)]);
1202
    }
1203
1204
    /**
1205
     * Checks whether a document is scheduled for upsert.
1206
     */
1207 5
    public function isScheduledForUpsert(object $document): bool
1208
    {
1209 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1210
    }
1211
1212
    /**
1213
     * Schedules a document for being updated.
1214
     *
1215
     * @throws \InvalidArgumentException
1216
     */
1217 236
    public function scheduleForUpdate(object $document): void
1218
    {
1219 236
        $oid = spl_object_hash($document);
1220 236
        if (! isset($this->documentIdentifiers[$oid])) {
1221
            throw new \InvalidArgumentException('Document has no identity.');
1222
        }
1223
1224 236
        if (isset($this->documentDeletions[$oid])) {
1225
            throw new \InvalidArgumentException('Document is removed.');
1226
        }
1227
1228 236
        if (isset($this->documentUpdates[$oid])
1229 236
            || isset($this->documentInsertions[$oid])
1230 236
            || isset($this->documentUpserts[$oid])) {
1231 98
            return;
1232
        }
1233
1234 234
        $this->documentUpdates[$oid] = $document;
1235 234
    }
1236
1237
    /**
1238
     * Checks whether a document is registered as dirty in the unit of work.
1239
     * Note: Is not very useful currently as dirty documents are only registered
1240
     * at commit time.
1241
     */
1242 21
    public function isScheduledForUpdate(object $document): bool
1243
    {
1244 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1245
    }
1246
1247 1
    public function isScheduledForDirtyCheck(object $document): bool
1248
    {
1249 1
        $class = $this->dm->getClassMetadata(get_class($document));
1250 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1251
    }
1252
1253
    /**
1254
     * INTERNAL:
1255
     * Schedules a document for deletion.
1256
     */
1257 79
    public function scheduleForDelete(object $document): void
1258
    {
1259 79
        $oid = spl_object_hash($document);
1260
1261 79
        if (isset($this->documentInsertions[$oid])) {
1262 2
            if ($this->isInIdentityMap($document)) {
1263 2
                $this->removeFromIdentityMap($document);
1264
            }
1265 2
            unset($this->documentInsertions[$oid]);
1266 2
            return; // document has not been persisted yet, so nothing more to do.
1267
        }
1268
1269 78
        if (! $this->isInIdentityMap($document)) {
1270 2
            return; // ignore
1271
        }
1272
1273 77
        $this->removeFromIdentityMap($document);
1274 77
        $this->documentStates[$oid] = self::STATE_REMOVED;
1275
1276 77
        if (isset($this->documentUpdates[$oid])) {
1277
            unset($this->documentUpdates[$oid]);
1278
        }
1279 77
        if (isset($this->documentDeletions[$oid])) {
1280
            return;
1281
        }
1282
1283 77
        $this->documentDeletions[$oid] = $document;
1284 77
    }
1285
1286
    /**
1287
     * Checks whether a document is registered as removed/deleted with the unit
1288
     * of work.
1289
     */
1290 5
    public function isScheduledForDelete(object $document): bool
1291
    {
1292 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1293
    }
1294
1295
    /**
1296
     * Checks whether a document is scheduled for insertion, update or deletion.
1297
     */
1298 250
    public function isDocumentScheduled(object $document): bool
1299
    {
1300 250
        $oid = spl_object_hash($document);
1301 250
        return isset($this->documentInsertions[$oid]) ||
1302 132
            isset($this->documentUpserts[$oid]) ||
1303 122
            isset($this->documentUpdates[$oid]) ||
1304 250
            isset($this->documentDeletions[$oid]);
1305
    }
1306
1307
    /**
1308
     * INTERNAL:
1309
     * Registers a document in the identity map.
1310
     *
1311
     * Note that documents in a hierarchy are registered with the class name of
1312
     * the root document. Identifiers are serialized before being used as array
1313
     * keys to allow differentiation of equal, but not identical, values.
1314
     *
1315
     * @ignore
1316
     */
1317 672
    public function addToIdentityMap(object $document): bool
1318
    {
1319 672
        $class = $this->dm->getClassMetadata(get_class($document));
1320 672
        $id = $this->getIdForIdentityMap($document);
1321
1322 672
        if (isset($this->identityMap[$class->name][$id])) {
1323 46
            return false;
1324
        }
1325
1326 672
        $this->identityMap[$class->name][$id] = $document;
1327
1328 672
        if ($document instanceof NotifyPropertyChanged &&
1329 672
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1330 3
            $document->addPropertyChangedListener($this);
1331
        }
1332
1333 672
        return true;
1334
    }
1335
1336
    /**
1337
     * Gets the state of a document with regard to the current unit of work.
1338
     *
1339
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1340
     *                         This parameter can be set to improve performance of document state detection
1341
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1342
     *                         is either known or does not matter for the caller of the method.
1343
     */
1344 637
    public function getDocumentState(object $document, ?int $assume = null): int
1345
    {
1346 637
        $oid = spl_object_hash($document);
1347
1348 637
        if (isset($this->documentStates[$oid])) {
1349 398
            return $this->documentStates[$oid];
1350
        }
1351
1352 636
        $class = $this->dm->getClassMetadata(get_class($document));
1353
1354 636
        if ($class->isEmbeddedDocument) {
1355 185
            return self::STATE_NEW;
1356
        }
1357
1358 633
        if ($assume !== null) {
1359 631
            return $assume;
1360
        }
1361
1362
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1363
         * known. Note that you cannot remember the NEW or DETACHED state in
1364
         * _documentStates since the UoW does not hold references to such
1365
         * objects and the object hash can be reused. More generally, because
1366
         * the state may "change" between NEW/DETACHED without the UoW being
1367
         * aware of it.
1368
         */
1369 3
        $id = $class->getIdentifierObject($document);
1370
1371 3
        if ($id === null) {
1372 2
            return self::STATE_NEW;
1373
        }
1374
1375
        // Check for a version field, if available, to avoid a DB lookup.
1376 2
        if ($class->isVersioned) {
1377
            return $class->getFieldValue($document, $class->versionField)
1378
                ? self::STATE_DETACHED
1379
                : self::STATE_NEW;
1380
        }
1381
1382
        // Last try before DB lookup: check the identity map.
1383 2
        if ($this->tryGetById($id, $class)) {
1384 1
            return self::STATE_DETACHED;
1385
        }
1386
1387
        // DB lookup
1388 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1389 1
            return self::STATE_DETACHED;
1390
        }
1391
1392 1
        return self::STATE_NEW;
1393
    }
1394
1395
    /**
1396
     * INTERNAL:
1397
     * Removes a document from the identity map. This effectively detaches the
1398
     * document from the persistence management of Doctrine.
1399
     *
1400
     * @ignore
1401
     * @throws \InvalidArgumentException
1402
     */
1403 91
    public function removeFromIdentityMap(object $document): bool
1404
    {
1405 91
        $oid = spl_object_hash($document);
1406
1407
        // Check if id is registered first
1408 91
        if (! isset($this->documentIdentifiers[$oid])) {
1409
            return false;
1410
        }
1411
1412 91
        $class = $this->dm->getClassMetadata(get_class($document));
1413 91
        $id = $this->getIdForIdentityMap($document);
1414
1415 91
        if (isset($this->identityMap[$class->name][$id])) {
1416 91
            unset($this->identityMap[$class->name][$id]);
1417 91
            $this->documentStates[$oid] = self::STATE_DETACHED;
1418 91
            return true;
1419
        }
1420
1421
        return false;
1422
    }
1423
1424
    /**
1425
     * INTERNAL:
1426
     * Gets a document in the identity map by its identifier hash.
1427
     *
1428
     * @ignore
1429
     * @param mixed $id Document identifier
1430
     * @throws InvalidArgumentException If the class does not have an identifier.
1431
     */
1432 40
    public function getById($id, ClassMetadata $class): object
1433
    {
1434 40
        if (! $class->identifier) {
1435
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1436
        }
1437
1438 40
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1439
1440 40
        return $this->identityMap[$class->name][$serializedId];
1441
    }
1442
1443
    /**
1444
     * INTERNAL:
1445
     * Tries to get a document by its identifier hash. If no document is found
1446
     * for the given hash, FALSE is returned.
1447
     *
1448
     * @ignore
1449
     * @param mixed $id Document identifier
1450
     * @return mixed The found document or FALSE.
1451
     * @throws InvalidArgumentException If the class does not have an identifier.
1452
     */
1453 319
    public function tryGetById($id, ClassMetadata $class)
1454
    {
1455 319
        if (! $class->identifier) {
1456
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1457
        }
1458
1459 319
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1460
1461 319
        return $this->identityMap[$class->name][$serializedId] ?? false;
1462
    }
1463
1464
    /**
1465
     * Schedules a document for dirty-checking at commit-time.
1466
     *
1467
     * @todo Rename: scheduleForSynchronization
1468
     */
1469 3
    public function scheduleForDirtyCheck(object $document): void
1470
    {
1471 3
        $class = $this->dm->getClassMetadata(get_class($document));
1472 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1473 3
    }
1474
1475
    /**
1476
     * Checks whether a document is registered in the identity map.
1477
     */
1478 87
    public function isInIdentityMap(object $document): bool
1479
    {
1480 87
        $oid = spl_object_hash($document);
1481
1482 87
        if (! isset($this->documentIdentifiers[$oid])) {
1483 6
            return false;
1484
        }
1485
1486 85
        $class = $this->dm->getClassMetadata(get_class($document));
1487 85
        $id = $this->getIdForIdentityMap($document);
1488
1489 85
        return isset($this->identityMap[$class->name][$id]);
1490
    }
1491
1492 672
    private function getIdForIdentityMap(object $document): string
1493
    {
1494 672
        $class = $this->dm->getClassMetadata(get_class($document));
1495
1496 672
        if (! $class->identifier) {
1497 157
            $id = spl_object_hash($document);
1498
        } else {
1499 671
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1500 671
            $id = serialize($class->getDatabaseIdentifierValue($id));
1501
        }
1502
1503 672
        return $id;
1504
    }
1505
1506
    /**
1507
     * INTERNAL:
1508
     * Checks whether an identifier exists in the identity map.
1509
     *
1510
     * @ignore
1511
     */
1512
    public function containsId($id, string $rootClassName): bool
1513
    {
1514
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1515
    }
1516
1517
    /**
1518
     * Persists a document as part of the current unit of work.
1519
     *
1520
     * @throws MongoDBException If trying to persist MappedSuperclass.
1521
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1522
     */
1523 634
    public function persist(object $document): void
1524
    {
1525 634
        $class = $this->dm->getClassMetadata(get_class($document));
1526 634
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1527 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1528
        }
1529 633
        $visited = [];
1530 633
        $this->doPersist($document, $visited);
1531 628
    }
1532
1533
    /**
1534
     * Saves a document as part of the current unit of work.
1535
     * This method is internally called during save() cascades as it tracks
1536
     * the already visited documents to prevent infinite recursions.
1537
     *
1538
     * NOTE: This method always considers documents that are not yet known to
1539
     * this UnitOfWork as NEW.
1540
     *
1541
     * @throws \InvalidArgumentException
1542
     * @throws MongoDBException
1543
     */
1544 633
    private function doPersist(object $document, array &$visited): void
1545
    {
1546 633
        $oid = spl_object_hash($document);
1547 633
        if (isset($visited[$oid])) {
1548 25
            return; // Prevent infinite recursion
1549
        }
1550
1551 633
        $visited[$oid] = $document; // Mark visited
1552
1553 633
        $class = $this->dm->getClassMetadata(get_class($document));
1554
1555 633
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1556
        switch ($documentState) {
1557 633
            case self::STATE_MANAGED:
1558
                // Nothing to do, except if policy is "deferred explicit"
1559 53
                if ($class->isChangeTrackingDeferredExplicit()) {
1560
                    $this->scheduleForDirtyCheck($document);
1561
                }
1562 53
                break;
1563 633
            case self::STATE_NEW:
1564 633
                if ($class->isFile) {
1565 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1566
                }
1567
1568 632
                $this->persistNew($class, $document);
1569 630
                break;
1570
1571 2
            case self::STATE_REMOVED:
1572
                // Document becomes managed again
1573 2
                unset($this->documentDeletions[$oid]);
1574
1575 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1576 2
                break;
1577
1578
            case self::STATE_DETACHED:
1579
                throw new \InvalidArgumentException(
1580
                    'Behavior of persist() for a detached document is not yet defined.'
1581
                );
1582
1583
            default:
1584
                throw MongoDBException::invalidDocumentState($documentState);
1585
        }
1586
1587 630
        $this->cascadePersist($document, $visited);
1588 628
    }
1589
1590
    /**
1591
     * Deletes a document as part of the current unit of work.
1592
     */
1593 78
    public function remove(object $document)
1594
    {
1595 78
        $visited = [];
1596 78
        $this->doRemove($document, $visited);
1597 78
    }
1598
1599
    /**
1600
     * Deletes a document as part of the current unit of work.
1601
     *
1602
     * This method is internally called during delete() cascades as it tracks
1603
     * the already visited documents to prevent infinite recursions.
1604
     *
1605
     * @throws MongoDBException
1606
     */
1607 78
    private function doRemove(object $document, array &$visited): void
1608
    {
1609 78
        $oid = spl_object_hash($document);
1610 78
        if (isset($visited[$oid])) {
1611 1
            return; // Prevent infinite recursion
1612
        }
1613
1614 78
        $visited[$oid] = $document; // mark visited
1615
1616
        /* Cascade first, because scheduleForDelete() removes the entity from
1617
         * the identity map, which can cause problems when a lazy Proxy has to
1618
         * be initialized for the cascade operation.
1619
         */
1620 78
        $this->cascadeRemove($document, $visited);
1621
1622 78
        $class = $this->dm->getClassMetadata(get_class($document));
1623 78
        $documentState = $this->getDocumentState($document);
1624
        switch ($documentState) {
1625 78
            case self::STATE_NEW:
1626 78
            case self::STATE_REMOVED:
1627
                // nothing to do
1628 1
                break;
1629 78
            case self::STATE_MANAGED:
1630 78
                $this->lifecycleEventManager->preRemove($class, $document);
1631 78
                $this->scheduleForDelete($document);
1632 78
                break;
1633
            case self::STATE_DETACHED:
1634
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1635
            default:
1636
                throw MongoDBException::invalidDocumentState($documentState);
1637
        }
1638 78
    }
1639
1640
    /**
1641
     * Merges the state of the given detached document into this UnitOfWork.
1642
     */
1643 12
    public function merge(object $document): object
1644
    {
1645 12
        $visited = [];
1646
1647 12
        return $this->doMerge($document, $visited);
1648
    }
1649
1650
    /**
1651
     * Executes a merge operation on a document.
1652
     *
1653
     * @throws InvalidArgumentException If the entity instance is NEW.
1654
     * @throws LockException If the document uses optimistic locking through a
1655
     *                       version attribute and the version check against the
1656
     *                       managed copy fails.
1657
     */
1658 12
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null): object
1659
    {
1660 12
        $oid = spl_object_hash($document);
1661
1662 12
        if (isset($visited[$oid])) {
1663 1
            return $visited[$oid]; // Prevent infinite recursion
1664
        }
1665
1666 12
        $visited[$oid] = $document; // mark visited
1667
1668 12
        $class = $this->dm->getClassMetadata(get_class($document));
1669
1670
        /* First we assume DETACHED, although it can still be NEW but we can
1671
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1672
         * an identity, we need to fetch it from the DB anyway in order to
1673
         * merge. MANAGED documents are ignored by the merge operation.
1674
         */
1675 12
        $managedCopy = $document;
1676
1677 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1678 12
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1679
                $document->__load();
1680
            }
1681
1682 12
            $identifier = $class->getIdentifier();
1683
            // We always have one element in the identifier array but it might be null
1684 12
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1685 12
            $managedCopy = null;
1686
1687
            // Try to fetch document from the database
1688 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1689 12
                $managedCopy = $this->dm->find($class->name, $id);
1690
1691
                // Managed copy may be removed in which case we can't merge
1692 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1693
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1694
                }
1695
1696 12
                if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
1697
                    $managedCopy->__load();
1698
                }
1699
            }
1700
1701 12
            if ($managedCopy === null) {
1702
                // Create a new managed instance
1703 4
                $managedCopy = $class->newInstance();
1704 4
                if ($id !== null) {
1705 3
                    $class->setIdentifierValue($managedCopy, $id);
1706
                }
1707 4
                $this->persistNew($class, $managedCopy);
1708
            }
1709
1710 12
            if ($class->isVersioned) {
1711
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1712
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1713
1714
                // Throw exception if versions don't match
1715
                if ($managedCopyVersion !== $documentVersion) {
1716
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1717
                }
1718
            }
1719
1720
            // Merge state of $document into existing (managed) document
1721 12
            foreach ($class->reflClass->getProperties() as $prop) {
1722 12
                $name = $prop->name;
1723 12
                $prop->setAccessible(true);
1724 12
                if (! isset($class->associationMappings[$name])) {
1725 12
                    if (! $class->isIdentifier($name)) {
1726 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1727
                    }
1728
                } else {
1729 12
                    $assoc2 = $class->associationMappings[$name];
1730
1731 12
                    if ($assoc2['type'] === 'one') {
1732 6
                        $other = $prop->getValue($document);
1733
1734 6
                        if ($other === null) {
1735 2
                            $prop->setValue($managedCopy, null);
1736 5
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1737
                            // Do not merge fields marked lazy that have not been fetched
1738 1
                            continue;
1739 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1740
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1741
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1742
                                /** @var ClassMetadata $targetClass */
1743
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1744
                                $relatedId = $targetClass->getIdentifierObject($other);
1745
1746
                                if ($targetClass->subClasses) {
1747
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1748
                                } else {
1749
                                    $other = $this
1750
                                        ->dm
1751
                                        ->getProxyFactory()
1752
                                        ->getProxy($assoc2['targetDocument'], [$targetClass->identifier => $relatedId]);
1753
                                    $this->registerManaged($other, $relatedId, []);
1754
                                }
1755
                            }
1756
1757 5
                            $prop->setValue($managedCopy, $other);
1758
                        }
1759
                    } else {
1760 10
                        $mergeCol = $prop->getValue($document);
1761
1762 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1763
                            /* Do not merge fields marked lazy that have not
1764
                             * been fetched. Keep the lazy persistent collection
1765
                             * of the managed copy.
1766
                             */
1767 3
                            continue;
1768
                        }
1769
1770 10
                        $managedCol = $prop->getValue($managedCopy);
1771
1772 10
                        if (! $managedCol) {
1773 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1774 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1775 1
                            $prop->setValue($managedCopy, $managedCol);
1776 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1777
                        }
1778
1779
                        /* Note: do not process association's target documents.
1780
                         * They will be handled during the cascade. Initialize
1781
                         * and, if necessary, clear $managedCol for now.
1782
                         */
1783 10
                        if ($assoc2['isCascadeMerge']) {
1784 10
                            $managedCol->initialize();
1785
1786
                            // If $managedCol differs from the merged collection, clear and set dirty
1787 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1788 3
                                $managedCol->unwrap()->clear();
1789 3
                                $managedCol->setDirty(true);
1790
1791 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1792
                                    $this->scheduleForDirtyCheck($managedCopy);
1793
                                }
1794
                            }
1795
                        }
1796
                    }
1797
                }
1798
1799 12
                if (! $class->isChangeTrackingNotify()) {
1800 12
                    continue;
1801
                }
1802
1803
                // Just treat all properties as changed, there is no other choice.
1804
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1805
            }
1806
1807 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1808
                $this->scheduleForDirtyCheck($document);
1809
            }
1810
        }
1811
1812 12
        if ($prevManagedCopy !== null) {
1813 5
            $assocField = $assoc['fieldName'];
1814 5
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1815
1816 5
            if ($assoc['type'] === 'one') {
1817 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1818
            } else {
1819 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1820
1821 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1822 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1823
                }
1824
            }
1825
        }
1826
1827
        // Mark the managed copy visited as well
1828 12
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1829
1830 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1831
1832 12
        return $managedCopy;
1833
    }
1834
1835
    /**
1836
     * Detaches a document from the persistence management. It's persistence will
1837
     * no longer be managed by Doctrine.
1838
     */
1839 11
    public function detach(object $document): void
1840
    {
1841 11
        $visited = [];
1842 11
        $this->doDetach($document, $visited);
1843 11
    }
1844
1845
    /**
1846
     * Executes a detach operation on the given document.
1847
     *
1848
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1849
     */
1850 17
    private function doDetach(object $document, array &$visited): void
1851
    {
1852 17
        $oid = spl_object_hash($document);
1853 17
        if (isset($visited[$oid])) {
1854 3
            return; // Prevent infinite recursion
1855
        }
1856
1857 17
        $visited[$oid] = $document; // mark visited
1858
1859 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1860 17
            case self::STATE_MANAGED:
1861 17
                $this->removeFromIdentityMap($document);
1862
                unset(
1863 17
                    $this->documentInsertions[$oid],
1864 17
                    $this->documentUpdates[$oid],
1865 17
                    $this->documentDeletions[$oid],
1866 17
                    $this->documentIdentifiers[$oid],
1867 17
                    $this->documentStates[$oid],
1868 17
                    $this->originalDocumentData[$oid],
1869 17
                    $this->parentAssociations[$oid],
1870 17
                    $this->documentUpserts[$oid],
1871 17
                    $this->hasScheduledCollections[$oid],
1872 17
                    $this->embeddedDocumentsRegistry[$oid]
1873
                );
1874 17
                break;
1875 3
            case self::STATE_NEW:
1876 3
            case self::STATE_DETACHED:
1877 3
                return;
1878
        }
1879
1880 17
        $this->cascadeDetach($document, $visited);
1881 17
    }
1882
1883
    /**
1884
     * Refreshes the state of the given document from the database, overwriting
1885
     * any local, unpersisted changes.
1886
     *
1887
     * @throws \InvalidArgumentException If the document is not MANAGED.
1888
     */
1889 24
    public function refresh(object $document): void
1890
    {
1891 24
        $visited = [];
1892 24
        $this->doRefresh($document, $visited);
1893 23
    }
1894
1895
    /**
1896
     * Executes a refresh operation on a document.
1897
     *
1898
     * @throws \InvalidArgumentException If the document is not MANAGED.
1899
     */
1900 24
    private function doRefresh(object $document, array &$visited): void
1901
    {
1902 24
        $oid = spl_object_hash($document);
1903 24
        if (isset($visited[$oid])) {
1904
            return; // Prevent infinite recursion
1905
        }
1906
1907 24
        $visited[$oid] = $document; // mark visited
1908
1909 24
        $class = $this->dm->getClassMetadata(get_class($document));
1910
1911 24
        if (! $class->isEmbeddedDocument) {
1912 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1913 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
1914
            }
1915
1916 23
            $this->getDocumentPersister($class->name)->refresh($document);
1917
        }
1918
1919 23
        $this->cascadeRefresh($document, $visited);
1920 23
    }
1921
1922
    /**
1923
     * Cascades a refresh operation to associated documents.
1924
     */
1925 23
    private function cascadeRefresh(object $document, array &$visited): void
1926
    {
1927 23
        $class = $this->dm->getClassMetadata(get_class($document));
1928
1929 23
        $associationMappings = array_filter(
1930 23
            $class->associationMappings,
1931
            function ($assoc) {
1932 18
                return $assoc['isCascadeRefresh'];
1933 23
            }
1934
        );
1935
1936 23
        foreach ($associationMappings as $mapping) {
1937 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1938 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
1939 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1940
                    // Unwrap so that foreach() does not initialize
1941 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1942
                }
1943 15
                foreach ($relatedDocuments as $relatedDocument) {
1944 15
                    $this->doRefresh($relatedDocument, $visited);
1945
                }
1946 10
            } elseif ($relatedDocuments !== null) {
1947 15
                $this->doRefresh($relatedDocuments, $visited);
1948
            }
1949
        }
1950 23
    }
1951
1952
    /**
1953
     * Cascades a detach operation to associated documents.
1954
     */
1955 17
    private function cascadeDetach(object $document, array &$visited): void
1956
    {
1957 17
        $class = $this->dm->getClassMetadata(get_class($document));
1958 17
        foreach ($class->fieldMappings as $mapping) {
1959 17
            if (! $mapping['isCascadeDetach']) {
1960 17
                continue;
1961
            }
1962 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1963 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
1964 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1965
                    // Unwrap so that foreach() does not initialize
1966 8
                    $relatedDocuments = $relatedDocuments->unwrap();
1967
                }
1968 11
                foreach ($relatedDocuments as $relatedDocument) {
1969 11
                    $this->doDetach($relatedDocument, $visited);
1970
                }
1971 11
            } elseif ($relatedDocuments !== null) {
1972 11
                $this->doDetach($relatedDocuments, $visited);
1973
            }
1974
        }
1975 17
    }
1976
    /**
1977
     * Cascades a merge operation to associated documents.
1978
     */
1979 12
    private function cascadeMerge(object $document, object $managedCopy, array &$visited): void
1980
    {
1981 12
        $class = $this->dm->getClassMetadata(get_class($document));
1982
1983 12
        $associationMappings = array_filter(
1984 12
            $class->associationMappings,
1985
            function ($assoc) {
1986 12
                return $assoc['isCascadeMerge'];
1987 12
            }
1988
        );
1989
1990 12
        foreach ($associationMappings as $assoc) {
1991 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
1992
1993 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
1994 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
1995
                    // Collections are the same, so there is nothing to do
1996 1
                    continue;
1997
                }
1998
1999 8
                foreach ($relatedDocuments as $relatedDocument) {
2000 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2001
                }
2002 6
            } elseif ($relatedDocuments !== null) {
2003 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2004
            }
2005
        }
2006 12
    }
2007
2008
    /**
2009
     * Cascades the save operation to associated documents.
2010
     */
2011 630
    private function cascadePersist(object $document, array &$visited): void
2012
    {
2013 630
        $class = $this->dm->getClassMetadata(get_class($document));
2014
2015 630
        $associationMappings = array_filter(
2016 630
            $class->associationMappings,
2017
            function ($assoc) {
2018 487
                return $assoc['isCascadePersist'];
2019 630
            }
2020
        );
2021
2022 630
        foreach ($associationMappings as $fieldName => $mapping) {
2023 435
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2024
2025 435
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2026 364
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2027 12
                    if ($relatedDocuments->getOwner() !== $document) {
2028 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2029
                    }
2030
                    // Unwrap so that foreach() does not initialize
2031 12
                    $relatedDocuments = $relatedDocuments->unwrap();
2032
                }
2033
2034 364
                $count = 0;
2035 364
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2036 194
                    if (! empty($mapping['embedded'])) {
2037 123
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2038 123
                        if ($knownParent && $knownParent !== $document) {
2039 1
                            $relatedDocument = clone $relatedDocument;
2040 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2041
                        }
2042 123
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2043 123
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2044
                    }
2045 364
                    $this->doPersist($relatedDocument, $visited);
2046
                }
2047 345
            } elseif ($relatedDocuments !== null) {
2048 130
                if (! empty($mapping['embedded'])) {
2049 69
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2050 69
                    if ($knownParent && $knownParent !== $document) {
2051 3
                        $relatedDocuments = clone $relatedDocuments;
2052 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2053
                    }
2054 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2055
                }
2056 435
                $this->doPersist($relatedDocuments, $visited);
2057
            }
2058
        }
2059 628
    }
2060
2061
    /**
2062
     * Cascades the delete operation to associated documents.
2063
     */
2064 78
    private function cascadeRemove(object $document, array &$visited): void
2065
    {
2066 78
        $class = $this->dm->getClassMetadata(get_class($document));
2067 78
        foreach ($class->fieldMappings as $mapping) {
2068 78
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2069 77
                continue;
2070
            }
2071 38
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
2072 3
                $document->__load();
2073
            }
2074
2075 38
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2076 38
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2077
                // If its a PersistentCollection initialization is intended! No unwrap!
2078 26
                foreach ($relatedDocuments as $relatedDocument) {
2079 26
                    $this->doRemove($relatedDocument, $visited);
2080
                }
2081 27
            } elseif ($relatedDocuments !== null) {
2082 38
                $this->doRemove($relatedDocuments, $visited);
2083
            }
2084
        }
2085 78
    }
2086
2087
    /**
2088
     * Acquire a lock on the given document.
2089
     *
2090
     * @throws LockException
2091
     * @throws \InvalidArgumentException
2092
     */
2093 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null): void
2094
    {
2095 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2096 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2097
        }
2098
2099 7
        $documentName = get_class($document);
2100 7
        $class = $this->dm->getClassMetadata($documentName);
2101
2102 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2103 2
            if (! $class->isVersioned) {
2104 1
                throw LockException::notVersioned($documentName);
2105
            }
2106
2107 1
            if ($lockVersion !== null) {
2108 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2109 1
                if ($documentVersion !== $lockVersion) {
2110 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2111
                }
2112
            }
2113 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2114 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2115
        }
2116 5
    }
2117
2118
    /**
2119
     * Releases a lock on the given document.
2120
     *
2121
     * @throws \InvalidArgumentException
2122
     */
2123 1
    public function unlock(object $document): void
2124
    {
2125 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2126
            throw new \InvalidArgumentException('Document is not MANAGED.');
2127
        }
2128 1
        $documentName = get_class($document);
2129 1
        $this->getDocumentPersister($documentName)->unlock($document);
2130 1
    }
2131
2132
    /**
2133
     * Clears the UnitOfWork.
2134
     */
2135 391
    public function clear(?string $documentName = null): void
2136
    {
2137 391
        if ($documentName === null) {
2138 385
            $this->identityMap =
2139 385
            $this->documentIdentifiers =
2140 385
            $this->originalDocumentData =
2141 385
            $this->documentChangeSets =
2142 385
            $this->documentStates =
2143 385
            $this->scheduledForDirtyCheck =
2144 385
            $this->documentInsertions =
2145 385
            $this->documentUpserts =
2146 385
            $this->documentUpdates =
2147 385
            $this->documentDeletions =
2148 385
            $this->collectionUpdates =
2149 385
            $this->collectionDeletions =
2150 385
            $this->parentAssociations =
2151 385
            $this->embeddedDocumentsRegistry =
2152 385
            $this->orphanRemovals =
2153 385
            $this->hasScheduledCollections = [];
2154
        } else {
2155 6
            $visited = [];
2156 6
            foreach ($this->identityMap as $className => $documents) {
2157 6
                if ($className !== $documentName) {
2158 3
                    continue;
2159
                }
2160
2161 6
                foreach ($documents as $document) {
2162 6
                    $this->doDetach($document, $visited);
2163
                }
2164
            }
2165
        }
2166
2167 391
        if (! $this->evm->hasListeners(Events::onClear)) {
2168 391
            return;
2169
        }
2170
2171
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2172
    }
2173
2174
    /**
2175
     * INTERNAL:
2176
     * Schedules an embedded document for removal. The remove() operation will be
2177
     * invoked on that document at the beginning of the next commit of this
2178
     * UnitOfWork.
2179
     *
2180
     * @ignore
2181
     */
2182 53
    public function scheduleOrphanRemoval(object $document): void
2183
    {
2184 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2185 53
    }
2186
2187
    /**
2188
     * INTERNAL:
2189
     * Unschedules an embedded or referenced object for removal.
2190
     *
2191
     * @ignore
2192
     */
2193 120
    public function unscheduleOrphanRemoval(object $document): void
2194
    {
2195 120
        $oid = spl_object_hash($document);
2196 120
        unset($this->orphanRemovals[$oid]);
2197 120
    }
2198
2199
    /**
2200
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2201
     *  1) sets owner if it was cloned
2202
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2203
     *  3) NOP if state is OK
2204
     * Returned collection should be used from now on (only important with 2nd point)
2205
     */
2206 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName): PersistentCollectionInterface
2207
    {
2208 8
        $owner = $coll->getOwner();
2209 8
        if ($owner === null) { // cloned
2210 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2211 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2212 2
            if (! $coll->isInitialized()) {
2213 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2214
            }
2215 2
            $newValue = clone $coll;
2216 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2217 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2218 2
            if ($this->isScheduledForUpdate($document)) {
2219
                // @todo following line should be superfluous once collections are stored in change sets
2220
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2221
            }
2222 2
            return $newValue;
2223
        }
2224 6
        return $coll;
2225
    }
2226
2227
    /**
2228
     * INTERNAL:
2229
     * Schedules a complete collection for removal when this UnitOfWork commits.
2230
     */
2231 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll): void
2232
    {
2233 43
        $oid = spl_object_hash($coll);
2234 43
        unset($this->collectionUpdates[$oid]);
2235 43
        if (isset($this->collectionDeletions[$oid])) {
2236
            return;
2237
        }
2238
2239 43
        $this->collectionDeletions[$oid] = $coll;
2240 43
        $this->scheduleCollectionOwner($coll);
2241 43
    }
2242
2243
    /**
2244
     * Checks whether a PersistentCollection is scheduled for deletion.
2245
     */
2246 213
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll): bool
2247
    {
2248 213
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2249
    }
2250
2251
    /**
2252
     * INTERNAL:
2253
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2254
     *
2255
     */
2256 225
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll): void
2257
    {
2258 225
        $oid = spl_object_hash($coll);
2259 225
        if (! isset($this->collectionDeletions[$oid])) {
2260 225
            return;
2261
        }
2262
2263 12
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2264 12
        unset($this->collectionDeletions[$oid]);
2265 12
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2266 12
    }
2267
2268
    /**
2269
     * INTERNAL:
2270
     * Schedules a collection for update when this UnitOfWork commits.
2271
     *
2272
     */
2273 247
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll): void
2274
    {
2275 247
        $mapping = $coll->getMapping();
2276 247
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2277
            /* There is no need to $unset collection if it will be $set later
2278
             * This is NOP if collection is not scheduled for deletion
2279
             */
2280 40
            $this->unscheduleCollectionDeletion($coll);
2281
        }
2282 247
        $oid = spl_object_hash($coll);
2283 247
        if (isset($this->collectionUpdates[$oid])) {
2284 11
            return;
2285
        }
2286
2287 247
        $this->collectionUpdates[$oid] = $coll;
2288 247
        $this->scheduleCollectionOwner($coll);
2289 247
    }
2290
2291
    /**
2292
     * INTERNAL:
2293
     * Unschedules a collection from being updated when this UnitOfWork commits.
2294
     *
2295
     */
2296 225
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll): void
2297
    {
2298 225
        $oid = spl_object_hash($coll);
2299 225
        if (! isset($this->collectionUpdates[$oid])) {
2300 45
            return;
2301
        }
2302
2303 215
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2304 215
        unset($this->collectionUpdates[$oid]);
2305 215
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2306 215
    }
2307
2308
    /**
2309
     * Checks whether a PersistentCollection is scheduled for update.
2310
     */
2311 133
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll): bool
2312
    {
2313 133
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2314
    }
2315
2316
    /**
2317
     * INTERNAL:
2318
     * Gets PersistentCollections that have been visited during computing change
2319
     * set of $document
2320
     *
2321
     * @return PersistentCollectionInterface[]
2322
     */
2323 583
    public function getVisitedCollections(object $document): array
2324
    {
2325 583
        $oid = spl_object_hash($document);
2326
2327 583
        return $this->visitedCollections[$oid] ?? [];
2328
    }
2329
2330
    /**
2331
     * INTERNAL:
2332
     * Gets PersistentCollections that are scheduled to update and related to $document
2333
     *
2334
     * @return PersistentCollectionInterface[]
2335
     */
2336 583
    public function getScheduledCollections(object $document): array
2337
    {
2338 583
        $oid = spl_object_hash($document);
2339
2340 583
        return $this->hasScheduledCollections[$oid] ?? [];
2341
    }
2342
2343
    /**
2344
     * Checks whether the document is related to a PersistentCollection
2345
     * scheduled for update or deletion.
2346
     */
2347 51
    public function hasScheduledCollections(object $document): bool
2348
    {
2349 51
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2350
    }
2351
2352
    /**
2353
     * Marks the PersistentCollection's top-level owner as having a relation to
2354
     * a collection scheduled for update or deletion.
2355
     *
2356
     * If the owner is not scheduled for any lifecycle action, it will be
2357
     * scheduled for update to ensure that versioning takes place if necessary.
2358
     *
2359
     * If the collection is nested within atomic collection, it is immediately
2360
     * unscheduled and atomic one is scheduled for update instead. This makes
2361
     * calculating update data way easier.
2362
     *
2363
     */
2364 249
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll): void
2365
    {
2366 249
        $document = $this->getOwningDocument($coll->getOwner());
2367 249
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2368
2369 249
        if ($document !== $coll->getOwner()) {
2370 25
            $parent = $coll->getOwner();
2371 25
            $mapping = [];
2372 25
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2373 25
                list($mapping, $parent, ) = $parentAssoc;
2374
            }
2375 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2376 8
                $class = $this->dm->getClassMetadata(get_class($document));
2377 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2378 8
                $this->scheduleCollectionUpdate($atomicCollection);
2379 8
                $this->unscheduleCollectionDeletion($coll);
2380 8
                $this->unscheduleCollectionUpdate($coll);
2381
            }
2382
        }
2383
2384 249
        if ($this->isDocumentScheduled($document)) {
2385 244
            return;
2386
        }
2387
2388 50
        $this->scheduleForUpdate($document);
2389 50
    }
2390
2391
    /**
2392
     * Get the top-most owning document of a given document
2393
     *
2394
     * If a top-level document is provided, that same document will be returned.
2395
     * For an embedded document, we will walk through parent associations until
2396
     * we find a top-level document.
2397
     *
2398
     * @throws \UnexpectedValueException When a top-level document could not be found.
2399
     */
2400 251
    public function getOwningDocument(object $document): object
2401
    {
2402 251
        $class = $this->dm->getClassMetadata(get_class($document));
2403 251
        while ($class->isEmbeddedDocument) {
2404 40
            $parentAssociation = $this->getParentAssociation($document);
2405
2406 40
            if (! $parentAssociation) {
2407
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2408
            }
2409
2410 40
            list(, $document, ) = $parentAssociation;
2411 40
            $class = $this->dm->getClassMetadata(get_class($document));
2412
        }
2413
2414 251
        return $document;
2415
    }
2416
2417
    /**
2418
     * Gets the class name for an association (embed or reference) with respect
2419
     * to any discriminator value.
2420
     *
2421
     * @param array|object|null $data
2422
     */
2423 240
    public function getClassNameForAssociation(array $mapping, $data): string
2424
    {
2425 240
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2426
2427 240
        $discriminatorValue = null;
2428 240
        if (isset($discriminatorField, $data[$discriminatorField])) {
2429 21
            $discriminatorValue = $data[$discriminatorField];
2430 220
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2431
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2432
        }
2433
2434 240
        if ($discriminatorValue !== null) {
2435 21
            return $mapping['discriminatorMap'][$discriminatorValue]
2436 21
                ?? $discriminatorValue;
2437
        }
2438
2439 220
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2440
2441 220
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2442 15
            $discriminatorValue = $data[$class->discriminatorField];
2443 206
        } elseif ($class->defaultDiscriminatorValue !== null) {
2444 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2445
        }
2446
2447 220
        if ($discriminatorValue !== null) {
2448 16
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2449
        }
2450
2451 205
        return $mapping['targetDocument'];
2452
    }
2453
2454
    /**
2455
     * INTERNAL:
2456
     * Creates a document. Used for reconstitution of documents during hydration.
2457
     *
2458
     * @ignore
2459
     * @internal Highly performance-sensitive method.
2460
     */
2461 409
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null): object
2462
    {
2463 409
        $class = $this->dm->getClassMetadata($className);
2464
2465
        // @TODO figure out how to remove this
2466 409
        $discriminatorValue = null;
2467 409
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2468 17
            $discriminatorValue = $data[$class->discriminatorField];
2469 401
        } elseif (isset($class->defaultDiscriminatorValue)) {
2470 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2471
        }
2472
2473 409
        if ($discriminatorValue !== null) {
2474 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2475
2476 18
            $class = $this->dm->getClassMetadata($className);
2477
2478 18
            unset($data[$class->discriminatorField]);
2479
        }
2480
2481 409
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2482 2
            $document = $class->newInstance();
2483 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2484 2
            return $document;
2485
        }
2486
2487 408
        $isManagedObject = false;
2488 408
        $serializedId = null;
2489 408
        $id = null;
2490 408
        if (! $class->isQueryResultDocument) {
2491 405
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2492 405
            $serializedId = serialize($id);
2493 405
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2494
        }
2495
2496 408
        $oid = null;
2497 408
        if ($isManagedObject) {
2498 97
            $document = $this->identityMap[$class->name][$serializedId];
2499 97
            $oid = spl_object_hash($document);
2500 97
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
2501 16
                $document->__isInitialized__ = true;
2502 16
                $overrideLocalValues = true;
2503 16
                if ($document instanceof NotifyPropertyChanged) {
2504 16
                    $document->addPropertyChangedListener($this);
2505
                }
2506
            } else {
2507 87
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2508
            }
2509 97
            if ($overrideLocalValues) {
2510 44
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2511 97
                $this->originalDocumentData[$oid] = $data;
2512
            }
2513
        } else {
2514 365
            if ($document === null) {
2515 365
                $document = $class->newInstance();
2516
            }
2517
2518 365
            if (! $class->isQueryResultDocument) {
2519 361
                $this->registerManaged($document, $id, $data);
2520 361
                $oid = spl_object_hash($document);
2521 361
                $this->documentStates[$oid] = self::STATE_MANAGED;
2522 361
                $this->identityMap[$class->name][$serializedId] = $document;
2523
            }
2524
2525 365
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2526
2527 365
            if (! $class->isQueryResultDocument) {
2528 361
                $this->originalDocumentData[$oid] = $data;
2529
            }
2530
        }
2531
2532 408
        return $document;
2533
    }
2534
2535
    /**
2536
     * Initializes (loads) an uninitialized persistent collection of a document.
2537
     */
2538 184
    public function loadCollection(PersistentCollectionInterface $collection): void
2539
    {
2540 184
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2541 184
        $this->lifecycleEventManager->postCollectionLoad($collection);
2542 184
    }
2543
2544
    /**
2545
     * Gets the identity map of the UnitOfWork.
2546
     */
2547
    public function getIdentityMap(): array
2548
    {
2549
        return $this->identityMap;
2550
    }
2551
2552
    /**
2553
     * Gets the original data of a document. The original data is the data that was
2554
     * present at the time the document was reconstituted from the database.
2555
     *
2556
     * @return array
2557
     */
2558 1
    public function getOriginalDocumentData(object $document): array
2559
    {
2560 1
        $oid = spl_object_hash($document);
2561
2562 1
        return $this->originalDocumentData[$oid] ?? [];
2563
    }
2564
2565 63
    public function setOriginalDocumentData(object $document, array $data): void
2566
    {
2567 63
        $oid = spl_object_hash($document);
2568 63
        $this->originalDocumentData[$oid] = $data;
2569 63
        unset($this->documentChangeSets[$oid]);
2570 63
    }
2571
2572
    /**
2573
     * INTERNAL:
2574
     * Sets a property value of the original data array of a document.
2575
     *
2576
     * @ignore
2577
     * @param mixed $value
2578
     */
2579 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value): void
2580
    {
2581 3
        $this->originalDocumentData[$oid][$property] = $value;
2582 3
    }
2583
2584
    /**
2585
     * Gets the identifier of a document.
2586
     *
2587
     * @return mixed The identifier value
2588
     */
2589 444
    public function getDocumentIdentifier(object $document)
2590
    {
2591 444
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2592
    }
2593
2594
    /**
2595
     * Checks whether the UnitOfWork has any pending insertions.
2596
     *
2597
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2598
     */
2599
    public function hasPendingInsertions(): bool
2600
    {
2601
        return ! empty($this->documentInsertions);
2602
    }
2603
2604
    /**
2605
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2606
     * number of documents in the identity map.
2607
     */
2608 2
    public function size(): int
2609
    {
2610 2
        $count = 0;
2611 2
        foreach ($this->identityMap as $documentSet) {
2612 2
            $count += count($documentSet);
2613
        }
2614 2
        return $count;
2615
    }
2616
2617
    /**
2618
     * INTERNAL:
2619
     * Registers a document as managed.
2620
     *
2621
     * TODO: This method assumes that $id is a valid PHP identifier for the
2622
     * document class. If the class expects its database identifier to be an
2623
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2624
     * document identifiers map will become inconsistent with the identity map.
2625
     * In the future, we may want to round-trip $id through a PHP and database
2626
     * conversion and throw an exception if it's inconsistent.
2627
     *
2628
     * @param mixed $id The identifier values.
2629
     */
2630 394
    public function registerManaged(object $document, $id, array $data): void
2631
    {
2632 394
        $oid = spl_object_hash($document);
2633 394
        $class = $this->dm->getClassMetadata(get_class($document));
2634
2635 394
        if (! $class->identifier || $id === null) {
2636 112
            $this->documentIdentifiers[$oid] = $oid;
2637
        } else {
2638 388
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2639
        }
2640
2641 394
        $this->documentStates[$oid] = self::STATE_MANAGED;
2642 394
        $this->originalDocumentData[$oid] = $data;
2643 394
        $this->addToIdentityMap($document);
2644 394
    }
2645
2646
    /**
2647
     * INTERNAL:
2648
     * Clears the property changeset of the document with the given OID.
2649
     */
2650
    public function clearDocumentChangeSet(string $oid)
2651
    {
2652
        $this->documentChangeSets[$oid] = [];
2653
    }
2654
2655
    /* PropertyChangedListener implementation */
2656
2657
    /**
2658
     * Notifies this UnitOfWork of a property change in a document.
2659
     *
2660
     * @param object $document     The document that owns the property.
2661
     * @param string $propertyName The name of the property that changed.
2662
     * @param mixed  $oldValue     The old value of the property.
2663
     * @param mixed  $newValue     The new value of the property.
2664
     */
2665 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2666
    {
2667 2
        $oid = spl_object_hash($document);
2668 2
        $class = $this->dm->getClassMetadata(get_class($document));
2669
2670 2
        if (! isset($class->fieldMappings[$propertyName])) {
2671 1
            return; // ignore non-persistent fields
2672
        }
2673
2674
        // Update changeset and mark document for synchronization
2675 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2676 2
        if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2677
            return;
2678
        }
2679
2680 2
        $this->scheduleForDirtyCheck($document);
2681 2
    }
2682
2683
    /**
2684
     * Gets the currently scheduled document insertions in this UnitOfWork.
2685
     */
2686 3
    public function getScheduledDocumentInsertions(): array
2687
    {
2688 3
        return $this->documentInsertions;
2689
    }
2690
2691
    /**
2692
     * Gets the currently scheduled document upserts in this UnitOfWork.
2693
     */
2694 1
    public function getScheduledDocumentUpserts(): array
2695
    {
2696 1
        return $this->documentUpserts;
2697
    }
2698
2699
    /**
2700
     * Gets the currently scheduled document updates in this UnitOfWork.
2701
     */
2702 2
    public function getScheduledDocumentUpdates(): array
2703
    {
2704 2
        return $this->documentUpdates;
2705
    }
2706
2707
    /**
2708
     * Gets the currently scheduled document deletions in this UnitOfWork.
2709
     */
2710
    public function getScheduledDocumentDeletions(): array
2711
    {
2712
        return $this->documentDeletions;
2713
    }
2714
2715
    /**
2716
     * Get the currently scheduled complete collection deletions
2717
     */
2718
    public function getScheduledCollectionDeletions(): array
2719
    {
2720
        return $this->collectionDeletions;
2721
    }
2722
2723
    /**
2724
     * Gets the currently scheduled collection inserts, updates and deletes.
2725
     */
2726
    public function getScheduledCollectionUpdates(): array
2727
    {
2728
        return $this->collectionUpdates;
2729
    }
2730
2731
    /**
2732
     * Helper method to initialize a lazy loading proxy or persistent collection.
2733
     */
2734
    public function initializeObject(object $obj): void
2735
    {
2736
        if ($obj instanceof Proxy) {
2737
            $obj->__load();
2738
        } elseif ($obj instanceof PersistentCollectionInterface) {
2739
            $obj->initialize();
2740
        }
2741
    }
2742
2743
    private function objToStr(object $obj): string
2744
    {
2745
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2746
    }
2747
}
2748