Completed
Push — master ( 23b3b5...5850fd )
by Andreas
11:22
created

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

Upgrade to new PHP Analysis Engine

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

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