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

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

Upgrade to new PHP Analysis Engine

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

1
<?php
2
/*
3
 * 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']) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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])
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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