Completed
Push — master ( f078de...f2e43e )
by Maciej
11s
created

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

Labels
Severity

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