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

lib/Doctrine/ODM/MongoDB/UnitOfWork.php (8 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)) {
0 ignored issues
show
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
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