Completed
Pull Request — master (#1647)
by Andreas
21:49 queued 12:05
created

UnitOfWork::getIdForIdentityMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 8
nc 2
nop 1
crap 2
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
     * @var int
265
     */
266
    private $commitsInProgress = 0;
267
268
    /**
269
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
270
     *
271
     * @param DocumentManager $dm
272
     * @param EventManager $evm
273
     * @param HydratorFactory $hydratorFactory
274
     */
275 1105
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
276
    {
277 1105
        $this->dm = $dm;
278 1105
        $this->evm = $evm;
279 1105
        $this->hydratorFactory = $hydratorFactory;
280 1105
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
281 1105
    }
282
283
    /**
284
     * Factory for returning new PersistenceBuilder instances used for preparing data into
285
     * queries for insert persistence.
286
     *
287
     * @return PersistenceBuilder $pb
288
     */
289 768
    public function getPersistenceBuilder()
290
    {
291 768
        if ( ! $this->persistenceBuilder) {
292 768
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
293
        }
294 768
        return $this->persistenceBuilder;
295
    }
296
297
    /**
298
     * Sets the parent association for a given embedded document.
299
     *
300
     * @param object $document
301
     * @param array $mapping
302
     * @param object $parent
303
     * @param string $propertyPath
304
     */
305 205
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
306
    {
307 205
        $oid = spl_object_hash($document);
308 205
        $this->embeddedDocumentsRegistry[$oid] = $document;
309 205
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
310 205
    }
311
312
    /**
313
     * Gets the parent association for a given embedded document.
314
     *
315
     *     <code>
316
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
317
     *     </code>
318
     *
319
     * @param object $document
320
     * @return array $association
321
     */
322 233
    public function getParentAssociation($document)
323
    {
324 233
        $oid = spl_object_hash($document);
325 233
        if ( ! isset($this->parentAssociations[$oid])) {
326 227
            return null;
327
        }
328 181
        return $this->parentAssociations[$oid];
329
    }
330
331
    /**
332
     * Get the document persister instance for the given document name
333
     *
334
     * @param string $documentName
335
     * @return Persisters\DocumentPersister
336
     */
337 766
    public function getDocumentPersister($documentName)
338
    {
339 766
        if ( ! isset($this->persisters[$documentName])) {
340 752
            $class = $this->dm->getClassMetadata($documentName);
341 752
            $pb = $this->getPersistenceBuilder();
342 752
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
343
        }
344 766
        return $this->persisters[$documentName];
345
    }
346
347
    /**
348
     * Get the collection persister instance.
349
     *
350
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
351
     */
352 766
    public function getCollectionPersister()
353
    {
354 766
        if ( ! isset($this->collectionPersister)) {
355 766
            $pb = $this->getPersistenceBuilder();
356 766
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
357
        }
358 766
        return $this->collectionPersister;
359
    }
360
361
    /**
362
     * Set the document persister instance to use for the given document name
363
     *
364
     * @param string $documentName
365
     * @param Persisters\DocumentPersister $persister
366
     */
367 14
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
368
    {
369 14
        $this->persisters[$documentName] = $persister;
370 14
    }
371
372
    /**
373
     * Commits the UnitOfWork, executing all operations that have been postponed
374
     * up to this point. The state of all managed documents will be synchronized with
375
     * the database.
376
     *
377
     * The operations are executed in the following order:
378
     *
379
     * 1) All document insertions
380
     * 2) All document updates
381
     * 3) All document deletions
382
     *
383
     * @param object $document
384
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
385
     */
386 632
    public function commit($document = null, array $options = array())
387
    {
388
        // Raise preFlush
389 632
        if ($this->evm->hasListeners(Events::preFlush)) {
390
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
391
        }
392
393
        // Compute changes done since last commit.
394 632
        if ($document === null) {
395 626
            $this->computeChangeSets();
396 14
        } elseif (is_object($document)) {
397 13
            $this->computeSingleDocumentChangeSet($document);
398 1
        } elseif (is_array($document)) {
399 1
            foreach ($document as $object) {
400 1
                $this->computeSingleDocumentChangeSet($object);
401
            }
402
        }
403
404 630
        if ( ! ($this->documentInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
411
        ) {
412 25
            return; // Nothing to do.
413
        }
414
415 627
        $this->commitsInProgress++;
416 627
        if ($this->commitsInProgress > 1) {
417
            @trigger_error('There is already a commit operation in progress. Calling flush in an event subscriber is deprecated and will be forbidden in 2.0.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
418
        }
419
        try {
420 627
            if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
421 50
                foreach ($this->orphanRemovals as $removal) {
422 50
                    $this->remove($removal);
423
                }
424
            }
425
426
            // Raise onFlush
427 627
            if ($this->evm->hasListeners(Events::onFlush)) {
428 7
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
429
            }
430
431 627
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
432 89
                list($class, $documents) = $classAndDocuments;
433 89
                $this->executeUpserts($class, $documents, $options);
434
            }
435
436 627
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
437 547
                list($class, $documents) = $classAndDocuments;
438 547
                $this->executeInserts($class, $documents, $options);
439
            }
440
441 626
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
442 237
                list($class, $documents) = $classAndDocuments;
443 237
                $this->executeUpdates($class, $documents, $options);
444
            }
445
446 625
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
447 72
                list($class, $documents) = $classAndDocuments;
448 72
                $this->executeDeletions($class, $documents, $options);
449
            }
450
451
            // Raise postFlush
452 625
            if ($this->evm->hasListeners(Events::postFlush)) {
453
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
454
            }
455
456
            // Clear up
457 625
            $this->documentInsertions =
458 625
            $this->documentUpserts =
459 625
            $this->documentUpdates =
460 625
            $this->documentDeletions =
461 625
            $this->documentChangeSets =
462 625
            $this->collectionUpdates =
463 625
            $this->collectionDeletions =
464 625
            $this->visitedCollections =
465 625
            $this->scheduledForDirtyCheck =
466 625
            $this->orphanRemovals =
467 625
            $this->hasScheduledCollections = array();
468 625
        } finally {
469 627
            $this->commitsInProgress--;
470
        }
471 625
    }
472
473
    /**
474
     * Groups a list of scheduled documents by their class.
475
     *
476
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
477
     * @param bool $includeEmbedded
478
     * @return array Tuples of ClassMetadata and a corresponding array of objects
479
     */
480 627
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
481
    {
482 627
        if (empty($documents)) {
483 627
            return array();
484
        }
485 626
        $divided = array();
486 626
        $embeds = array();
487 626
        foreach ($documents as $oid => $d) {
488 626
            $className = get_class($d);
489 626
            if (isset($embeds[$className])) {
490 78
                continue;
491
            }
492 626
            if (isset($divided[$className])) {
493 160
                $divided[$className][1][$oid] = $d;
494 160
                continue;
495
            }
496 626
            $class = $this->dm->getClassMetadata($className);
497 626
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
498 183
                $embeds[$className] = true;
499 183
                continue;
500
            }
501 626
            if (empty($divided[$class->name])) {
502 626
                $divided[$class->name] = array($class, array($oid => $d));
503
            } else {
504 626
                $divided[$class->name][1][$oid] = $d;
505
            }
506
        }
507 626
        return $divided;
508
    }
509
510
    /**
511
     * Compute changesets of all documents scheduled for insertion.
512
     *
513
     * Embedded documents will not be processed.
514
     */
515 634 View Code Duplication
    private function computeScheduleInsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
516
    {
517 634
        foreach ($this->documentInsertions as $document) {
518 558
            $class = $this->dm->getClassMetadata(get_class($document));
519 558
            if ( ! $class->isEmbeddedDocument) {
520 558
                $this->computeChangeSet($class, $document);
521
            }
522
        }
523 633
    }
524
525
    /**
526
     * Compute changesets of all documents scheduled for upsert.
527
     *
528
     * Embedded documents will not be processed.
529
     */
530 633 View Code Duplication
    private function computeScheduleUpsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
531
    {
532 633
        foreach ($this->documentUpserts as $document) {
533 88
            $class = $this->dm->getClassMetadata(get_class($document));
534 88
            if ( ! $class->isEmbeddedDocument) {
535 88
                $this->computeChangeSet($class, $document);
536
            }
537
        }
538 633
    }
539
540
    /**
541
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
542
     *
543
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
544
     * 2. Proxies are skipped.
545
     * 3. Only if document is properly managed.
546
     *
547
     * @param  object $document
548
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
549
     * @return void
550
     */
551 14
    private function computeSingleDocumentChangeSet($document)
552
    {
553 14
        $state = $this->getDocumentState($document);
554
555 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
556 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
557
        }
558
559 13
        $class = $this->dm->getClassMetadata(get_class($document));
560
561 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
562 10
            $this->persist($document);
563
        }
564
565
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
566 13
        $this->computeScheduleInsertsChangeSets();
567 13
        $this->computeScheduleUpsertsChangeSets();
568
569
        // Ignore uninitialized proxy objects
570 13
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
571
            return;
572
        }
573
574
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
575 13
        $oid = spl_object_hash($document);
576
577 13 View Code Duplication
        if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
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...
578 13
            && ! isset($this->documentUpserts[$oid])
579 13
            && ! isset($this->documentDeletions[$oid])
580 13
            && isset($this->documentStates[$oid])
581
        ) {
582 8
            $this->computeChangeSet($class, $document);
583
        }
584 13
    }
585
586
    /**
587
     * Gets the changeset for a document.
588
     *
589
     * @param object $document
590
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
591
     */
592 627
    public function getDocumentChangeSet($document)
593
    {
594 627
        $oid = spl_object_hash($document);
595 627
        if (isset($this->documentChangeSets[$oid])) {
596 624
            return $this->documentChangeSets[$oid];
597
        }
598 62
        return array();
599
    }
600
601
    /**
602
     * INTERNAL:
603
     * Sets the changeset for a document.
604
     *
605
     * @param object $document
606
     * @param array $changeset
607
     */
608 1
    public function setDocumentChangeSet($document, $changeset)
609
    {
610 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
611 1
    }
612
613
    /**
614
     * Get a documents actual data, flattening all the objects to arrays.
615
     *
616
     * @param object $document
617
     * @return array
618
     */
619 634
    public function getDocumentActualData($document)
620
    {
621 634
        $class = $this->dm->getClassMetadata(get_class($document));
622 634
        $actualData = array();
623 634
        foreach ($class->reflFields as $name => $refProp) {
624 634
            $mapping = $class->fieldMappings[$name];
625
            // skip not saved fields
626 634
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
627 54
                continue;
628
            }
629 634
            $value = $refProp->getValue($document);
630 634
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
631 7
                $value = new GridFSFile($value);
632 7
                $class->reflFields[$name]->setValue($document, $value);
633 7
                $actualData[$name] = $value;
634 634
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
635 634
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
636
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
637 408
                if ( ! $value instanceof Collection) {
638 144
                    $value = new ArrayCollection($value);
639
                }
640
641
                // Inject PersistentCollection
642 408
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
643 408
                $coll->setOwner($document, $mapping);
644 408
                $coll->setDirty( ! $value->isEmpty());
645 408
                $class->reflFields[$name]->setValue($document, $coll);
646 408
                $actualData[$name] = $coll;
647
            } else {
648 634
                $actualData[$name] = $value;
649
            }
650
        }
651 634
        return $actualData;
652
    }
653
654
    /**
655
     * Computes the changes that happened to a single document.
656
     *
657
     * Modifies/populates the following properties:
658
     *
659
     * {@link originalDocumentData}
660
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
661
     * then it was not fetched from the database and therefore we have no original
662
     * document data yet. All of the current document data is stored as the original document data.
663
     *
664
     * {@link documentChangeSets}
665
     * The changes detected on all properties of the document are stored there.
666
     * A change is a tuple array where the first entry is the old value and the second
667
     * entry is the new value of the property. Changesets are used by persisters
668
     * to INSERT/UPDATE the persistent document state.
669
     *
670
     * {@link documentUpdates}
671
     * If the document is already fully MANAGED (has been fetched from the database before)
672
     * and any changes to its properties are detected, then a reference to the document is stored
673
     * there to mark it for an update.
674
     *
675
     * @param ClassMetadata $class The class descriptor of the document.
676
     * @param object $document The document for which to compute the changes.
677
     */
678 631
    public function computeChangeSet(ClassMetadata $class, $document)
679
    {
680 631
        if ( ! $class->isInheritanceTypeNone()) {
681 192
            $class = $this->dm->getClassMetadata(get_class($document));
682
        }
683
684
        // Fire PreFlush lifecycle callbacks
685 631 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
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...
686 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
687
        }
688
689 631
        $this->computeOrRecomputeChangeSet($class, $document);
690 630
    }
691
692
    /**
693
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
694
     *
695
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
696
     * @param object $document
697
     * @param boolean $recompute
698
     */
699 631
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
700
    {
701 631
        $oid = spl_object_hash($document);
702 631
        $actualData = $this->getDocumentActualData($document);
703 631
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
704 631
        if ($isNewDocument) {
705
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
706
            // These result in an INSERT.
707 629
            $this->originalDocumentData[$oid] = $actualData;
708 629
            $changeSet = array();
709 629
            foreach ($actualData as $propName => $actualValue) {
710
                /* At this PersistentCollection shouldn't be here, probably it
711
                 * was cloned and its ownership must be fixed
712
                 */
713 629
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
714
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
715
                    $actualValue = $actualData[$propName];
716
                }
717
                // ignore inverse side of reference relationship
718 629 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
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...
719 198
                    continue;
720
                }
721 629
                $changeSet[$propName] = array(null, $actualValue);
722
            }
723 629
            $this->documentChangeSets[$oid] = $changeSet;
724
        } else {
725
            // Document is "fully" MANAGED: it was already fully persisted before
726
            // and we have a copy of the original data
727 299
            $originalData = $this->originalDocumentData[$oid];
728 299
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
729 299
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
730 2
                $changeSet = $this->documentChangeSets[$oid];
731
            } else {
732 299
                $changeSet = array();
733
            }
734
735 299
            foreach ($actualData as $propName => $actualValue) {
736
                // skip not saved fields
737 299
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
738
                    continue;
739
                }
740
741 299
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
742
743
                // skip if value has not changed
744 299
                if ($orgValue === $actualValue) {
745 298
                    if ($actualValue instanceof PersistentCollectionInterface) {
746 205
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
747
                            // consider dirty collections as changed as well
748 205
                            continue;
749
                        }
750 298
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
751
                        // but consider dirty GridFSFile instances as changed
752 298
                        continue;
753
                    }
754
                }
755
756
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
757 256
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
758 13
                    if ($orgValue !== null) {
759 8
                        $this->scheduleOrphanRemoval($orgValue);
760
                    }
761 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
762 13
                    continue;
763
                }
764
765
                // if owning side of reference-one relationship
766 250
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
767 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
768 1
                        $this->scheduleOrphanRemoval($orgValue);
769
                    }
770
771 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
772 13
                    continue;
773
                }
774
775 243
                if ($isChangeTrackingNotify) {
776 3
                    continue;
777
                }
778
779
                // ignore inverse side of reference relationship
780 241 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
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...
781 6
                    continue;
782
                }
783
784
                // Persistent collection was exchanged with the "originally"
785
                // created one. This can only mean it was cloned and replaced
786
                // on another document.
787 239
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
788 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
789
                }
790
791
                // if embed-many or reference-many relationship
792 239
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
793 119
                    $changeSet[$propName] = array($orgValue, $actualValue);
794
                    /* If original collection was exchanged with a non-empty value
795
                     * and $set will be issued, there is no need to $unset it first
796
                     */
797 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
798 28
                        continue;
799
                    }
800 99
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
801 18
                        $this->scheduleCollectionDeletion($orgValue);
802
                    }
803 99
                    continue;
804
                }
805
806
                // skip equivalent date values
807 157
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
808 37
                    $dateType = Type::getType('date');
809 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
810 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
811
812 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
813 30
                        continue;
814
                    }
815
                }
816
817
                // regular field
818 140
                $changeSet[$propName] = array($orgValue, $actualValue);
819
            }
820 299
            if ($changeSet) {
821 245
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
822 21
                    ? $changeSet + $this->documentChangeSets[$oid]
823 240
                    : $changeSet;
824
825 245
                $this->originalDocumentData[$oid] = $actualData;
826 245
                $this->scheduleForUpdate($document);
827
            }
828
        }
829
830
        // Look for changes in associations of the document
831 631
        $associationMappings = array_filter(
832 631
            $class->associationMappings,
833
            function ($assoc) { return empty($assoc['notSaved']); }
834
        );
835
836 631
        foreach ($associationMappings as $mapping) {
837 480
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
838
839 480
            if ($value === null) {
840 325
                continue;
841
            }
842
843 467
            $this->computeAssociationChanges($document, $mapping, $value);
844
845 466
            if (isset($mapping['reference'])) {
846 353
                continue;
847
            }
848
849 363
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
850
851 363
            foreach ($values as $obj) {
852 187
                $oid2 = spl_object_hash($obj);
853
854 187
                if (isset($this->documentChangeSets[$oid2])) {
855 185
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
856
                        // instance of $value is the same as it was previously otherwise there would be
857
                        // change set already in place
858 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
859
                    }
860
861 185
                    if ( ! $isNewDocument) {
862 80
                        $this->scheduleForUpdate($document);
863
                    }
864
865 363
                    break;
866
                }
867
            }
868
        }
869 630
    }
870
871
    /**
872
     * Computes all the changes that have been done to documents and collections
873
     * since the last commit and stores these changes in the _documentChangeSet map
874
     * temporarily for access by the persisters, until the UoW commit is finished.
875
     */
876 629
    public function computeChangeSets()
877
    {
878 629
        $this->computeScheduleInsertsChangeSets();
879 628
        $this->computeScheduleUpsertsChangeSets();
880
881
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
882 628
        foreach ($this->identityMap as $className => $documents) {
883 628
            $class = $this->dm->getClassMetadata($className);
884 628
            if ($class->isEmbeddedDocument) {
885
                /* we do not want to compute changes to embedded documents up front
886
                 * in case embedded document was replaced and its changeset
887
                 * would corrupt data. Embedded documents' change set will
888
                 * be calculated by reachability from owning document.
889
                 */
890 176
                continue;
891
            }
892
893
            // If change tracking is explicit or happens through notification, then only compute
894
            // changes on document of that type that are explicitly marked for synchronization.
895
            switch (true) {
896 628
                case ($class->isChangeTrackingDeferredImplicit()):
897 627
                    $documentsToProcess = $documents;
898 627
                    break;
899
900 4
                case (isset($this->scheduledForDirtyCheck[$className])):
901 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
902 3
                    break;
903
904
                default:
905 4
                    $documentsToProcess = array();
906
907
            }
908
909 628
            foreach ($documentsToProcess as $document) {
910
                // Ignore uninitialized proxy objects
911 624
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
912 10
                    continue;
913
                }
914
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
915 624
                $oid = spl_object_hash($document);
916 624 View Code Duplication
                if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
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...
917 624
                    && ! isset($this->documentUpserts[$oid])
918 624
                    && ! isset($this->documentDeletions[$oid])
919 624
                    && isset($this->documentStates[$oid])
920
                ) {
921 628
                    $this->computeChangeSet($class, $document);
922
                }
923
            }
924
        }
925 628
    }
926
927
    /**
928
     * Computes the changes of an association.
929
     *
930
     * @param object $parentDocument
931
     * @param array $assoc
932
     * @param mixed $value The value of the association.
933
     * @throws \InvalidArgumentException
934
     */
935 467
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
936
    {
937 467
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
938 467
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
939 467
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
940
941 467
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
942 8
            return;
943
        }
944
945 466
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
946 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
947 254
                $this->scheduleCollectionUpdate($value);
948
            }
949 258
            $topmostOwner = $this->getOwningDocument($value->getOwner());
950 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
951 258
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
952 147
                $value->initialize();
953 147
                foreach ($value->getDeletedDocuments() as $orphan) {
954 23
                    $this->scheduleOrphanRemoval($orphan);
955
                }
956
            }
957
        }
958
959
        // Look through the documents, and in any of their associations,
960
        // for transient (new) documents, recursively. ("Persistence by reachability")
961
        // Unwrap. Uninitialized collections will simply be empty.
962 466
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
963
964 466
        $count = 0;
965 466
        foreach ($unwrappedValue as $key => $entry) {
966 371
            if ( ! is_object($entry)) {
967 1
                throw new \InvalidArgumentException(
968 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
969
                );
970
            }
971
972 370
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
973
974 370
            $state = $this->getDocumentState($entry, self::STATE_NEW);
975
976
            // Handle "set" strategy for multi-level hierarchy
977 370
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
978 370
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
979
980 370
            $count++;
981
982
            switch ($state) {
983 370
                case self::STATE_NEW:
984 68
                    if ( ! $assoc['isCascadePersist']) {
985
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
986
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
987
                            . ' Explicitly persist the new document or configure cascading persist operations'
988
                            . ' on the relationship.');
989
                    }
990
991 68
                    $this->persistNew($targetClass, $entry);
992 68
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
993 68
                    $this->computeChangeSet($targetClass, $entry);
994 68
                    break;
995
996 365
                case self::STATE_MANAGED:
997 365
                    if ($targetClass->isEmbeddedDocument) {
998 178
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
999 178
                        if ($knownParent && $knownParent !== $parentDocument) {
1000 9
                            $entry = clone $entry;
1001 9
                            if ($assoc['type'] === ClassMetadata::ONE) {
1002 6
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
1003 6
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
1004 6
                                $poid = spl_object_hash($parentDocument);
1005 6
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
1006 6
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
1007
                                }
1008
                            } else {
1009
                                // must use unwrapped value to not trigger orphan removal
1010 7
                                $unwrappedValue[$key] = $entry;
1011
                            }
1012 9
                            $this->persistNew($targetClass, $entry);
1013
                        }
1014 178
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
1015 178
                        $this->computeChangeSet($targetClass, $entry);
1016
                    }
1017 365
                    break;
1018
1019 1
                case self::STATE_REMOVED:
1020
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1021
                    // and remove the element from Collection.
1022 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1023
                        unset($value[$key]);
1024
                    }
1025 1
                    break;
1026
1027
                case self::STATE_DETACHED:
1028
                    // Can actually not happen right now as we assume STATE_NEW,
1029
                    // so the exception will be raised from the DBAL layer (constraint violation).
1030
                    throw new \InvalidArgumentException('A detached document was found through a '
1031
                        . 'relationship during cascading a persist operation.');
1032
1033 370
                default:
1034
                    // MANAGED associated documents are already taken into account
1035
                    // during changeset calculation anyway, since they are in the identity map.
1036
1037
            }
1038
        }
1039 465
    }
1040
1041
    /**
1042
     * INTERNAL:
1043
     * Computes the changeset of an individual document, independently of the
1044
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1045
     *
1046
     * The passed document must be a managed document. If the document already has a change set
1047
     * because this method is invoked during a commit cycle then the change sets are added.
1048
     * whereby changes detected in this method prevail.
1049
     *
1050
     * @ignore
1051
     * @param ClassMetadata $class The class descriptor of the document.
1052
     * @param object $document The document for which to (re)calculate the change set.
1053
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1054
     */
1055 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1056
    {
1057
        // Ignore uninitialized proxy objects
1058 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
1059 1
            return;
1060
        }
1061
1062 19
        $oid = spl_object_hash($document);
1063
1064 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1065
            throw new \InvalidArgumentException('Document must be managed.');
1066
        }
1067
1068 19
        if ( ! $class->isInheritanceTypeNone()) {
1069 2
            $class = $this->dm->getClassMetadata(get_class($document));
1070
        }
1071
1072 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1073 19
    }
1074
1075
    /**
1076
     * @param ClassMetadata $class
1077
     * @param object $document
1078
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1079
     */
1080 664
    private function persistNew(ClassMetadata $class, $document)
1081
    {
1082 664
        $this->lifecycleEventManager->prePersist($class, $document);
1083 664
        $oid = spl_object_hash($document);
1084 664
        $upsert = false;
1085 664
        if ($class->identifier) {
1086 664
            $idValue = $class->getIdentifierValue($document);
1087 664
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1088
1089 664
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1090 3
                throw new \InvalidArgumentException(sprintf(
1091 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1092 3
                    get_class($document)
1093
                ));
1094
            }
1095
1096 663
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1097 1
                throw new \InvalidArgumentException(sprintf(
1098 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1099 1
                    get_class($document)
1100
                ));
1101
            }
1102
1103 662
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1104 579
                $idValue = $class->idGenerator->generate($this->dm, $document);
1105 579
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1106 579
                $class->setIdentifierValue($document, $idValue);
1107
            }
1108
1109 662
            $this->documentIdentifiers[$oid] = $idValue;
1110
        } else {
1111
            // this is for embedded documents without identifiers
1112 161
            $this->documentIdentifiers[$oid] = $oid;
1113
        }
1114
1115 662
        $this->documentStates[$oid] = self::STATE_MANAGED;
1116
1117 662
        if ($upsert) {
1118 93
            $this->scheduleForUpsert($class, $document);
1119
        } else {
1120 588
            $this->scheduleForInsert($class, $document);
1121
        }
1122 662
    }
1123
1124
    /**
1125
     * Executes all document insertions for documents of the specified type.
1126
     *
1127
     * @param ClassMetadata $class
1128
     * @param array $documents Array of documents to insert
1129
     * @param array $options Array of options to be used with batchInsert()
1130
     */
1131 547 View Code Duplication
    private function executeInserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1132
    {
1133 547
        $persister = $this->getDocumentPersister($class->name);
1134
1135 547
        foreach ($documents as $oid => $document) {
1136 547
            $persister->addInsert($document);
1137 547
            unset($this->documentInsertions[$oid]);
1138
        }
1139
1140 547
        $persister->executeInserts($options);
1141
1142 546
        foreach ($documents as $document) {
1143 546
            $this->lifecycleEventManager->postPersist($class, $document);
1144
        }
1145 546
    }
1146
1147
    /**
1148
     * Executes all document upserts for documents of the specified type.
1149
     *
1150
     * @param ClassMetadata $class
1151
     * @param array $documents Array of documents to upsert
1152
     * @param array $options Array of options to be used with batchInsert()
1153
     */
1154 89 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
1155
    {
1156 89
        $persister = $this->getDocumentPersister($class->name);
1157
1158
1159 89
        foreach ($documents as $oid => $document) {
1160 89
            $persister->addUpsert($document);
1161 89
            unset($this->documentUpserts[$oid]);
1162
        }
1163
1164 89
        $persister->executeUpserts($options);
1165
1166 89
        foreach ($documents as $document) {
1167 89
            $this->lifecycleEventManager->postPersist($class, $document);
1168
        }
1169 89
    }
1170
1171
    /**
1172
     * Executes all document updates for documents of the specified type.
1173
     *
1174
     * @param Mapping\ClassMetadata $class
1175
     * @param array $documents Array of documents to update
1176
     * @param array $options Array of options to be used with update()
1177
     */
1178 237
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1179
    {
1180 237
        $className = $class->name;
1181 237
        $persister = $this->getDocumentPersister($className);
1182
1183 237
        foreach ($documents as $oid => $document) {
1184 237
            $this->lifecycleEventManager->preUpdate($class, $document);
1185
1186 237
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1187 235
                $persister->update($document, $options);
1188
            }
1189
1190 230
            unset($this->documentUpdates[$oid]);
1191
1192 230
            $this->lifecycleEventManager->postUpdate($class, $document);
1193
        }
1194 229
    }
1195
1196
    /**
1197
     * Executes all document deletions for documents of the specified type.
1198
     *
1199
     * @param ClassMetadata $class
1200
     * @param array $documents Array of documents to delete
1201
     * @param array $options Array of options to be used with remove()
1202
     */
1203 72
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1204
    {
1205 72
        $persister = $this->getDocumentPersister($class->name);
1206
1207 72
        foreach ($documents as $oid => $document) {
1208 72
            if ( ! $class->isEmbeddedDocument) {
1209 34
                $persister->delete($document, $options);
1210
            }
1211
            unset(
1212 70
                $this->documentDeletions[$oid],
1213 70
                $this->documentIdentifiers[$oid],
1214 70
                $this->originalDocumentData[$oid]
1215
            );
1216
1217
            // Clear snapshot information for any referenced PersistentCollection
1218
            // http://www.doctrine-project.org/jira/browse/MODM-95
1219 70
            foreach ($class->associationMappings as $fieldMapping) {
1220 45
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1221 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1222 27
                    if ($value instanceof PersistentCollectionInterface) {
1223 45
                        $value->clearSnapshot();
1224
                    }
1225
                }
1226
            }
1227
1228
            // Document with this $oid after deletion treated as NEW, even if the $oid
1229
            // is obtained by a new document because the old one went out of scope.
1230 70
            $this->documentStates[$oid] = self::STATE_NEW;
1231
1232 70
            $this->lifecycleEventManager->postRemove($class, $document);
1233
        }
1234 70
    }
1235
1236
    /**
1237
     * Schedules a document for insertion into the database.
1238
     * If the document already has an identifier, it will be added to the
1239
     * identity map.
1240
     *
1241
     * @param ClassMetadata $class
1242
     * @param object $document The document to schedule for insertion.
1243
     * @throws \InvalidArgumentException
1244
     */
1245 591
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1246
    {
1247 591
        $oid = spl_object_hash($document);
1248
1249 591
        if (isset($this->documentUpdates[$oid])) {
1250
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1251
        }
1252 591
        if (isset($this->documentDeletions[$oid])) {
1253
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1254
        }
1255 591
        if (isset($this->documentInsertions[$oid])) {
1256
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1257
        }
1258
1259 591
        $this->documentInsertions[$oid] = $document;
1260
1261 591
        if (isset($this->documentIdentifiers[$oid])) {
1262 588
            $this->addToIdentityMap($document);
1263
        }
1264 591
    }
1265
1266
    /**
1267
     * Schedules a document for upsert into the database and adds it to the
1268
     * identity map
1269
     *
1270
     * @param ClassMetadata $class
1271
     * @param object $document The document to schedule for upsert.
1272
     * @throws \InvalidArgumentException
1273
     */
1274 96
    public function scheduleForUpsert(ClassMetadata $class, $document)
1275
    {
1276 96
        $oid = spl_object_hash($document);
1277
1278 96
        if ($class->isEmbeddedDocument) {
1279
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1280
        }
1281 96
        if (isset($this->documentUpdates[$oid])) {
1282
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1283
        }
1284 96
        if (isset($this->documentDeletions[$oid])) {
1285
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1286
        }
1287 96
        if (isset($this->documentUpserts[$oid])) {
1288
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1289
        }
1290
1291 96
        $this->documentUpserts[$oid] = $document;
1292 96
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1293 96
        $this->addToIdentityMap($document);
1294 96
    }
1295
1296
    /**
1297
     * Checks whether a document is scheduled for insertion.
1298
     *
1299
     * @param object $document
1300
     * @return boolean
1301
     */
1302 108
    public function isScheduledForInsert($document)
1303
    {
1304 108
        return isset($this->documentInsertions[spl_object_hash($document)]);
1305
    }
1306
1307
    /**
1308
     * Checks whether a document is scheduled for upsert.
1309
     *
1310
     * @param object $document
1311
     * @return boolean
1312
     */
1313 5
    public function isScheduledForUpsert($document)
1314
    {
1315 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1316
    }
1317
1318
    /**
1319
     * Schedules a document for being updated.
1320
     *
1321
     * @param object $document The document to schedule for being updated.
1322
     * @throws \InvalidArgumentException
1323
     */
1324 246
    public function scheduleForUpdate($document)
1325
    {
1326 246
        $oid = spl_object_hash($document);
1327 246
        if ( ! isset($this->documentIdentifiers[$oid])) {
1328
            throw new \InvalidArgumentException('Document has no identity.');
1329
        }
1330
1331 246
        if (isset($this->documentDeletions[$oid])) {
1332
            throw new \InvalidArgumentException('Document is removed.');
1333
        }
1334
1335 246
        if ( ! isset($this->documentUpdates[$oid])
1336 246
            && ! isset($this->documentInsertions[$oid])
1337 246
            && ! isset($this->documentUpserts[$oid])) {
1338 242
            $this->documentUpdates[$oid] = $document;
1339
        }
1340 246
    }
1341
1342
    /**
1343
     * Checks whether a document is registered as dirty in the unit of work.
1344
     * Note: Is not very useful currently as dirty documents are only registered
1345
     * at commit time.
1346
     *
1347
     * @param object $document
1348
     * @return boolean
1349
     */
1350 22
    public function isScheduledForUpdate($document)
1351
    {
1352 22
        return isset($this->documentUpdates[spl_object_hash($document)]);
1353
    }
1354
1355 1
    public function isScheduledForDirtyCheck($document)
1356
    {
1357 1
        $class = $this->dm->getClassMetadata(get_class($document));
1358 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1359
    }
1360
1361
    /**
1362
     * INTERNAL:
1363
     * Schedules a document for deletion.
1364
     *
1365
     * @param object $document
1366
     */
1367 77
    public function scheduleForDelete($document)
1368
    {
1369 77
        $oid = spl_object_hash($document);
1370
1371 77
        if (isset($this->documentInsertions[$oid])) {
1372 2
            if ($this->isInIdentityMap($document)) {
1373 2
                $this->removeFromIdentityMap($document);
1374
            }
1375 2
            unset($this->documentInsertions[$oid]);
1376 2
            return; // document has not been persisted yet, so nothing more to do.
1377
        }
1378
1379 76
        if ( ! $this->isInIdentityMap($document)) {
1380 2
            return; // ignore
1381
        }
1382
1383 75
        $this->removeFromIdentityMap($document);
1384 75
        $this->documentStates[$oid] = self::STATE_REMOVED;
1385
1386 75
        if (isset($this->documentUpdates[$oid])) {
1387
            unset($this->documentUpdates[$oid]);
1388
        }
1389 75
        if ( ! isset($this->documentDeletions[$oid])) {
1390 75
            $this->documentDeletions[$oid] = $document;
1391
        }
1392 75
    }
1393
1394
    /**
1395
     * Checks whether a document is registered as removed/deleted with the unit
1396
     * of work.
1397
     *
1398
     * @param object $document
1399
     * @return boolean
1400
     */
1401 8
    public function isScheduledForDelete($document)
1402
    {
1403 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1404
    }
1405
1406
    /**
1407
     * Checks whether a document is scheduled for insertion, update or deletion.
1408
     *
1409
     * @param $document
1410
     * @return boolean
1411
     */
1412 257
    public function isDocumentScheduled($document)
1413
    {
1414 257
        $oid = spl_object_hash($document);
1415 257
        return isset($this->documentInsertions[$oid]) ||
1416 132
            isset($this->documentUpserts[$oid]) ||
1417 122
            isset($this->documentUpdates[$oid]) ||
1418 257
            isset($this->documentDeletions[$oid]);
1419
    }
1420
1421
    /**
1422
     * INTERNAL:
1423
     * Registers a document in the identity map.
1424
     *
1425
     * Note that documents in a hierarchy are registered with the class name of
1426
     * the root document. Identifiers are serialized before being used as array
1427
     * keys to allow differentiation of equal, but not identical, values.
1428
     *
1429
     * @ignore
1430
     * @param object $document  The document to register.
1431
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1432
     *                  the document in question is already managed.
1433
     */
1434 695
    public function addToIdentityMap($document)
1435
    {
1436 695
        $class = $this->dm->getClassMetadata(get_class($document));
1437 695
        $id = $this->getIdForIdentityMap($document);
1438
1439 695
        if (isset($this->identityMap[$class->name][$id])) {
1440 56
            return false;
1441
        }
1442
1443 695
        $this->identityMap[$class->name][$id] = $document;
1444
1445 695
        if ($document instanceof NotifyPropertyChanged &&
1446 695
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1447 4
            $document->addPropertyChangedListener($this);
1448
        }
1449
1450 695
        return true;
1451
    }
1452
1453
    /**
1454
     * Gets the state of a document with regard to the current unit of work.
1455
     *
1456
     * @param object   $document
1457
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1458
     *                         This parameter can be set to improve performance of document state detection
1459
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1460
     *                         is either known or does not matter for the caller of the method.
1461
     * @return int The document state.
1462
     */
1463 669
    public function getDocumentState($document, $assume = null)
1464
    {
1465 669
        $oid = spl_object_hash($document);
1466
1467 669
        if (isset($this->documentStates[$oid])) {
1468 416
            return $this->documentStates[$oid];
1469
        }
1470
1471 667
        $class = $this->dm->getClassMetadata(get_class($document));
1472
1473 667
        if ($class->isEmbeddedDocument) {
1474 196
            return self::STATE_NEW;
1475
        }
1476
1477 664
        if ($assume !== null) {
1478 661
            return $assume;
1479
        }
1480
1481
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1482
         * known. Note that you cannot remember the NEW or DETACHED state in
1483
         * _documentStates since the UoW does not hold references to such
1484
         * objects and the object hash can be reused. More generally, because
1485
         * the state may "change" between NEW/DETACHED without the UoW being
1486
         * aware of it.
1487
         */
1488 4
        $id = $class->getIdentifierObject($document);
1489
1490 4
        if ($id === null) {
1491 3
            return self::STATE_NEW;
1492
        }
1493
1494
        // Check for a version field, if available, to avoid a DB lookup.
1495 2
        if ($class->isVersioned) {
1496
            return $class->getFieldValue($document, $class->versionField)
1497
                ? self::STATE_DETACHED
1498
                : self::STATE_NEW;
1499
        }
1500
1501
        // Last try before DB lookup: check the identity map.
1502 2
        if ($this->tryGetById($id, $class)) {
1503 1
            return self::STATE_DETACHED;
1504
        }
1505
1506
        // DB lookup
1507 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1508 1
            return self::STATE_DETACHED;
1509
        }
1510
1511 1
        return self::STATE_NEW;
1512
    }
1513
1514
    /**
1515
     * INTERNAL:
1516
     * Removes a document from the identity map. This effectively detaches the
1517
     * document from the persistence management of Doctrine.
1518
     *
1519
     * @ignore
1520
     * @param object $document
1521
     * @throws \InvalidArgumentException
1522
     * @return boolean
1523
     */
1524 90
    public function removeFromIdentityMap($document)
1525
    {
1526 90
        $oid = spl_object_hash($document);
1527
1528
        // Check if id is registered first
1529 90
        if ( ! isset($this->documentIdentifiers[$oid])) {
1530
            return false;
1531
        }
1532
1533 90
        $class = $this->dm->getClassMetadata(get_class($document));
1534 90
        $id = $this->getIdForIdentityMap($document);
1535
1536 90
        if (isset($this->identityMap[$class->name][$id])) {
1537 90
            unset($this->identityMap[$class->name][$id]);
1538 90
            $this->documentStates[$oid] = self::STATE_DETACHED;
1539 90
            return true;
1540
        }
1541
1542
        return false;
1543
    }
1544
1545
    /**
1546
     * INTERNAL:
1547
     * Gets a document in the identity map by its identifier hash.
1548
     *
1549
     * @ignore
1550
     * @param mixed         $id    Document identifier
1551
     * @param ClassMetadata $class Document class
1552
     * @return object
1553
     * @throws InvalidArgumentException if the class does not have an identifier
1554
     */
1555 34
    public function getById($id, ClassMetadata $class)
1556
    {
1557 34
        if ( ! $class->identifier) {
1558
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1559
        }
1560
1561 34
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1562
1563 34
        return $this->identityMap[$class->name][$serializedId];
1564
    }
1565
1566
    /**
1567
     * INTERNAL:
1568
     * Tries to get a document by its identifier hash. If no document is found
1569
     * for the given hash, FALSE is returned.
1570
     *
1571
     * @ignore
1572
     * @param mixed         $id    Document identifier
1573
     * @param ClassMetadata $class Document class
1574
     * @return mixed The found document or FALSE.
1575
     * @throws InvalidArgumentException if the class does not have an identifier
1576
     */
1577 312
    public function tryGetById($id, ClassMetadata $class)
1578
    {
1579 312
        if ( ! $class->identifier) {
1580
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1581
        }
1582
1583 312
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1584
1585 312
        return isset($this->identityMap[$class->name][$serializedId]) ?
1586 312
            $this->identityMap[$class->name][$serializedId] : false;
1587
    }
1588
1589
    /**
1590
     * Schedules a document for dirty-checking at commit-time.
1591
     *
1592
     * @param object $document The document to schedule for dirty-checking.
1593
     * @todo Rename: scheduleForSynchronization
1594
     */
1595 3
    public function scheduleForDirtyCheck($document)
1596
    {
1597 3
        $class = $this->dm->getClassMetadata(get_class($document));
1598 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1599 3
    }
1600
1601
    /**
1602
     * Checks whether a document is registered in the identity map.
1603
     *
1604
     * @param object $document
1605
     * @return boolean
1606
     */
1607 88
    public function isInIdentityMap($document)
1608
    {
1609 88
        $oid = spl_object_hash($document);
1610
1611 88
        if ( ! isset($this->documentIdentifiers[$oid])) {
1612 6
            return false;
1613
        }
1614
1615 86
        $class = $this->dm->getClassMetadata(get_class($document));
1616 86
        $id = $this->getIdForIdentityMap($document);
1617
1618 86
        return isset($this->identityMap[$class->name][$id]);
1619
    }
1620
1621
    /**
1622
     * @param object $document
1623
     * @return string
1624
     */
1625 695
    private function getIdForIdentityMap($document)
1626
    {
1627 695
        $class = $this->dm->getClassMetadata(get_class($document));
1628
1629 695
        if ( ! $class->identifier) {
1630 164
            $id = spl_object_hash($document);
1631
        } else {
1632 694
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1633 694
            $id = serialize($class->getDatabaseIdentifierValue($id));
1634
        }
1635
1636 695
        return $id;
1637
    }
1638
1639
    /**
1640
     * INTERNAL:
1641
     * Checks whether an identifier exists in the identity map.
1642
     *
1643
     * @ignore
1644
     * @param string $id
1645
     * @param string $rootClassName
1646
     * @return boolean
1647
     */
1648
    public function containsId($id, $rootClassName)
1649
    {
1650
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1651
    }
1652
1653
    /**
1654
     * Persists a document as part of the current unit of work.
1655
     *
1656
     * @param object $document The document to persist.
1657
     * @throws MongoDBException If trying to persist MappedSuperclass.
1658
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1659
     */
1660 663
    public function persist($document)
1661
    {
1662 663
        $class = $this->dm->getClassMetadata(get_class($document));
1663 663
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1664 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1665
        }
1666 662
        $visited = array();
1667 662
        $this->doPersist($document, $visited);
1668 658
    }
1669
1670
    /**
1671
     * Saves a document as part of the current unit of work.
1672
     * This method is internally called during save() cascades as it tracks
1673
     * the already visited documents to prevent infinite recursions.
1674
     *
1675
     * NOTE: This method always considers documents that are not yet known to
1676
     * this UnitOfWork as NEW.
1677
     *
1678
     * @param object $document The document to persist.
1679
     * @param array $visited The already visited documents.
1680
     * @throws \InvalidArgumentException
1681
     * @throws MongoDBException
1682
     */
1683 662
    private function doPersist($document, array &$visited)
1684
    {
1685 662
        $oid = spl_object_hash($document);
1686 662
        if (isset($visited[$oid])) {
1687 24
            return; // Prevent infinite recursion
1688
        }
1689
1690 662
        $visited[$oid] = $document; // Mark visited
1691
1692 662
        $class = $this->dm->getClassMetadata(get_class($document));
1693
1694 662
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1695
        switch ($documentState) {
1696 662
            case self::STATE_MANAGED:
1697
                // Nothing to do, except if policy is "deferred explicit"
1698 61
                if ($class->isChangeTrackingDeferredExplicit()) {
1699
                    $this->scheduleForDirtyCheck($document);
1700
                }
1701 61
                break;
1702 660
            case self::STATE_NEW:
1703 660
                $this->persistNew($class, $document);
1704 658
                break;
1705
1706 2
            case self::STATE_REMOVED:
1707
                // Document becomes managed again
1708 2
                unset($this->documentDeletions[$oid]);
1709
1710 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1711 2
                break;
1712
1713
            case self::STATE_DETACHED:
1714
                throw new \InvalidArgumentException(
1715
                    'Behavior of persist() for a detached document is not yet defined.');
1716
1717
            default:
1718
                throw MongoDBException::invalidDocumentState($documentState);
1719
        }
1720
1721 660
        $this->cascadePersist($document, $visited);
1722 658
    }
1723
1724
    /**
1725
     * Deletes a document as part of the current unit of work.
1726
     *
1727
     * @param object $document The document to remove.
1728
     */
1729 76
    public function remove($document)
1730
    {
1731 76
        $visited = array();
1732 76
        $this->doRemove($document, $visited);
1733 76
    }
1734
1735
    /**
1736
     * Deletes a document as part of the current unit of work.
1737
     *
1738
     * This method is internally called during delete() cascades as it tracks
1739
     * the already visited documents to prevent infinite recursions.
1740
     *
1741
     * @param object $document The document to delete.
1742
     * @param array $visited The map of the already visited documents.
1743
     * @throws MongoDBException
1744
     */
1745 76
    private function doRemove($document, array &$visited)
1746
    {
1747 76
        $oid = spl_object_hash($document);
1748 76
        if (isset($visited[$oid])) {
1749 1
            return; // Prevent infinite recursion
1750
        }
1751
1752 76
        $visited[$oid] = $document; // mark visited
1753
1754
        /* Cascade first, because scheduleForDelete() removes the entity from
1755
         * the identity map, which can cause problems when a lazy Proxy has to
1756
         * be initialized for the cascade operation.
1757
         */
1758 76
        $this->cascadeRemove($document, $visited);
1759
1760 76
        $class = $this->dm->getClassMetadata(get_class($document));
1761 76
        $documentState = $this->getDocumentState($document);
1762
        switch ($documentState) {
1763 76
            case self::STATE_NEW:
1764 76
            case self::STATE_REMOVED:
1765
                // nothing to do
1766 1
                break;
1767 76
            case self::STATE_MANAGED:
1768 76
                $this->lifecycleEventManager->preRemove($class, $document);
1769 76
                $this->scheduleForDelete($document);
1770 76
                break;
1771
            case self::STATE_DETACHED:
1772
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1773
            default:
1774
                throw MongoDBException::invalidDocumentState($documentState);
1775
        }
1776 76
    }
1777
1778
    /**
1779
     * Merges the state of the given detached document into this UnitOfWork.
1780
     *
1781
     * @param object $document
1782
     * @return object The managed copy of the document.
1783
     */
1784 15
    public function merge($document)
1785
    {
1786 15
        $visited = array();
1787
1788 15
        return $this->doMerge($document, $visited);
1789
    }
1790
1791
    /**
1792
     * Executes a merge operation on a document.
1793
     *
1794
     * @param object      $document
1795
     * @param array       $visited
1796
     * @param object|null $prevManagedCopy
1797
     * @param array|null  $assoc
1798
     *
1799
     * @return object The managed copy of the document.
1800
     *
1801
     * @throws InvalidArgumentException If the entity instance is NEW.
1802
     * @throws LockException If the document uses optimistic locking through a
1803
     *                       version attribute and the version check against the
1804
     *                       managed copy fails.
1805
     */
1806 15
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1807
    {
1808 15
        $oid = spl_object_hash($document);
1809
1810 15
        if (isset($visited[$oid])) {
1811 1
            return $visited[$oid]; // Prevent infinite recursion
1812
        }
1813
1814 15
        $visited[$oid] = $document; // mark visited
1815
1816 15
        $class = $this->dm->getClassMetadata(get_class($document));
1817
1818
        /* First we assume DETACHED, although it can still be NEW but we can
1819
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1820
         * an identity, we need to fetch it from the DB anyway in order to
1821
         * merge. MANAGED documents are ignored by the merge operation.
1822
         */
1823 15
        $managedCopy = $document;
1824
1825 15
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1826 15
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1827
                $document->__load();
1828
            }
1829
1830 15
            $identifier = $class->getIdentifier();
1831
            // We always have one element in the identifier array but it might be null
1832 15
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1833 15
            $managedCopy = null;
1834
1835
            // Try to fetch document from the database
1836 15
            if (! $class->isEmbeddedDocument && $id !== null) {
1837 12
                $managedCopy = $this->dm->find($class->name, $id);
1838
1839
                // Managed copy may be removed in which case we can't merge
1840 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1841
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1842
                }
1843
1844 12
                if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
1845
                    $managedCopy->__load();
1846
                }
1847
            }
1848
1849 15
            if ($managedCopy === null) {
1850
                // Create a new managed instance
1851 7
                $managedCopy = $class->newInstance();
1852 7
                if ($id !== null) {
1853 3
                    $class->setIdentifierValue($managedCopy, $id);
1854
                }
1855 7
                $this->persistNew($class, $managedCopy);
1856
            }
1857
1858 15
            if ($class->isVersioned) {
1859
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1860
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1861
1862
                // Throw exception if versions don't match
1863
                if ($managedCopyVersion != $documentVersion) {
1864
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1865
                }
1866
            }
1867
1868
            // Merge state of $document into existing (managed) document
1869 15
            foreach ($class->reflClass->getProperties() as $prop) {
1870 15
                $name = $prop->name;
1871 15
                $prop->setAccessible(true);
1872 15
                if ( ! isset($class->associationMappings[$name])) {
1873 15
                    if ( ! $class->isIdentifier($name)) {
1874 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1875
                    }
1876
                } else {
1877 15
                    $assoc2 = $class->associationMappings[$name];
1878
1879 15
                    if ($assoc2['type'] === 'one') {
1880 7
                        $other = $prop->getValue($document);
1881
1882 7
                        if ($other === null) {
1883 2
                            $prop->setValue($managedCopy, null);
1884 6
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
1885
                            // Do not merge fields marked lazy that have not been fetched
1886 1
                            continue;
1887 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1888
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1889
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1890
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1891
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1892
                                $relatedId = $targetClass->getIdentifierObject($other);
1893
1894
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1895
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1896
                                } else {
1897
                                    $other = $this
1898
                                        ->dm
1899
                                        ->getProxyFactory()
1900
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1901
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1902
                                }
1903
                            }
1904
1905 6
                            $prop->setValue($managedCopy, $other);
1906
                        }
1907
                    } else {
1908 12
                        $mergeCol = $prop->getValue($document);
1909
1910 12
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1911
                            /* Do not merge fields marked lazy that have not
1912
                             * been fetched. Keep the lazy persistent collection
1913
                             * of the managed copy.
1914
                             */
1915 3
                            continue;
1916
                        }
1917
1918 12
                        $managedCol = $prop->getValue($managedCopy);
1919
1920 12
                        if ( ! $managedCol) {
1921 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1922 3
                            $managedCol->setOwner($managedCopy, $assoc2);
1923 3
                            $prop->setValue($managedCopy, $managedCol);
1924 3
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1925
                        }
1926
1927
                        /* Note: do not process association's target documents.
1928
                         * They will be handled during the cascade. Initialize
1929
                         * and, if necessary, clear $managedCol for now.
1930
                         */
1931 12
                        if ($assoc2['isCascadeMerge']) {
1932 12
                            $managedCol->initialize();
1933
1934
                            // If $managedCol differs from the merged collection, clear and set dirty
1935 12
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1936 3
                                $managedCol->unwrap()->clear();
1937 3
                                $managedCol->setDirty(true);
1938
1939 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1940
                                    $this->scheduleForDirtyCheck($managedCopy);
1941
                                }
1942
                            }
1943
                        }
1944
                    }
1945
                }
1946
1947 15
                if ($class->isChangeTrackingNotify()) {
1948
                    // Just treat all properties as changed, there is no other choice.
1949 15
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1950
                }
1951
            }
1952
1953 15
            if ($class->isChangeTrackingDeferredExplicit()) {
1954
                $this->scheduleForDirtyCheck($document);
1955
            }
1956
        }
1957
1958 15
        if ($prevManagedCopy !== null) {
1959 8
            $assocField = $assoc['fieldName'];
1960 8
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1961
1962 8
            if ($assoc['type'] === 'one') {
1963 4
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1964
            } else {
1965 6
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1966
1967 6
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1968 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1969
                }
1970
            }
1971
        }
1972
1973
        // Mark the managed copy visited as well
1974 15
        $visited[spl_object_hash($managedCopy)] = true;
1975
1976 15
        $this->cascadeMerge($document, $managedCopy, $visited);
1977
1978 15
        return $managedCopy;
1979
    }
1980
1981
    /**
1982
     * Detaches a document from the persistence management. It's persistence will
1983
     * no longer be managed by Doctrine.
1984
     *
1985
     * @param object $document The document to detach.
1986
     */
1987 12
    public function detach($document)
1988
    {
1989 12
        $visited = array();
1990 12
        $this->doDetach($document, $visited);
1991 12
    }
1992
1993
    /**
1994
     * Executes a detach operation on the given document.
1995
     *
1996
     * @param object $document
1997
     * @param array $visited
1998
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1999
     */
2000 17
    private function doDetach($document, array &$visited)
2001
    {
2002 17
        $oid = spl_object_hash($document);
2003 17
        if (isset($visited[$oid])) {
2004 4
            return; // Prevent infinite recursion
2005
        }
2006
2007 17
        $visited[$oid] = $document; // mark visited
2008
2009 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2010 17
            case self::STATE_MANAGED:
2011 17
                $this->removeFromIdentityMap($document);
2012 17
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2013 17
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2014 17
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2015 17
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2016 17
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2017 17
                break;
2018 4
            case self::STATE_NEW:
2019 4
            case self::STATE_DETACHED:
2020 4
                return;
2021
        }
2022
2023 17
        $this->cascadeDetach($document, $visited);
2024 17
    }
2025
2026
    /**
2027
     * Refreshes the state of the given document from the database, overwriting
2028
     * any local, unpersisted changes.
2029
     *
2030
     * @param object $document The document to refresh.
2031
     * @throws \InvalidArgumentException If the document is not MANAGED.
2032
     */
2033 23
    public function refresh($document)
2034
    {
2035 23
        $visited = array();
2036 23
        $this->doRefresh($document, $visited);
2037 22
    }
2038
2039
    /**
2040
     * Executes a refresh operation on a document.
2041
     *
2042
     * @param object $document The document to refresh.
2043
     * @param array $visited The already visited documents during cascades.
2044
     * @throws \InvalidArgumentException If the document is not MANAGED.
2045
     */
2046 23
    private function doRefresh($document, array &$visited)
2047
    {
2048 23
        $oid = spl_object_hash($document);
2049 23
        if (isset($visited[$oid])) {
2050
            return; // Prevent infinite recursion
2051
        }
2052
2053 23
        $visited[$oid] = $document; // mark visited
2054
2055 23
        $class = $this->dm->getClassMetadata(get_class($document));
2056
2057 23
        if ( ! $class->isEmbeddedDocument) {
2058 23
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2059 22
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2060 22
                $this->getDocumentPersister($class->name)->refresh($id, $document);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Per...entPersister::refresh() has been deprecated with message: The first argument is deprecated.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2061
            } else {
2062 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2063
            }
2064
        }
2065
2066 22
        $this->cascadeRefresh($document, $visited);
2067 22
    }
2068
2069
    /**
2070
     * Cascades a refresh operation to associated documents.
2071
     *
2072
     * @param object $document
2073
     * @param array $visited
2074
     */
2075 22
    private function cascadeRefresh($document, array &$visited)
2076
    {
2077 22
        $class = $this->dm->getClassMetadata(get_class($document));
2078
2079 22
        $associationMappings = array_filter(
2080 22
            $class->associationMappings,
2081
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2082
        );
2083
2084 22
        foreach ($associationMappings as $mapping) {
2085 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2086 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2087 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2088
                    // Unwrap so that foreach() does not initialize
2089 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2090
                }
2091 15
                foreach ($relatedDocuments as $relatedDocument) {
2092 15
                    $this->doRefresh($relatedDocument, $visited);
2093
                }
2094 10
            } elseif ($relatedDocuments !== null) {
2095 15
                $this->doRefresh($relatedDocuments, $visited);
2096
            }
2097
        }
2098 22
    }
2099
2100
    /**
2101
     * Cascades a detach operation to associated documents.
2102
     *
2103
     * @param object $document
2104
     * @param array $visited
2105
     */
2106 17 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2107
    {
2108 17
        $class = $this->dm->getClassMetadata(get_class($document));
2109 17
        foreach ($class->fieldMappings as $mapping) {
2110 17
            if ( ! $mapping['isCascadeDetach']) {
2111 17
                continue;
2112
            }
2113 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2114 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2115 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2116
                    // Unwrap so that foreach() does not initialize
2117 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2118
                }
2119 11
                foreach ($relatedDocuments as $relatedDocument) {
2120 11
                    $this->doDetach($relatedDocument, $visited);
2121
                }
2122 11
            } elseif ($relatedDocuments !== null) {
2123 11
                $this->doDetach($relatedDocuments, $visited);
2124
            }
2125
        }
2126 17
    }
2127
    /**
2128
     * Cascades a merge operation to associated documents.
2129
     *
2130
     * @param object $document
2131
     * @param object $managedCopy
2132
     * @param array $visited
2133
     */
2134 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2135
    {
2136 15
        $class = $this->dm->getClassMetadata(get_class($document));
2137
2138 15
        $associationMappings = array_filter(
2139 15
            $class->associationMappings,
2140
            function ($assoc) { return $assoc['isCascadeMerge']; }
2141
        );
2142
2143 15
        foreach ($associationMappings as $assoc) {
2144 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2145
2146 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2147 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2148
                    // Collections are the same, so there is nothing to do
2149 1
                    continue;
2150
                }
2151
2152 10
                foreach ($relatedDocuments as $relatedDocument) {
2153 10
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2154
                }
2155 7
            } elseif ($relatedDocuments !== null) {
2156 14
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2157
            }
2158
        }
2159 15
    }
2160
2161
    /**
2162
     * Cascades the save operation to associated documents.
2163
     *
2164
     * @param object $document
2165
     * @param array $visited
2166
     */
2167 660
    private function cascadePersist($document, array &$visited)
2168
    {
2169 660
        $class = $this->dm->getClassMetadata(get_class($document));
2170
2171 660
        $associationMappings = array_filter(
2172 660
            $class->associationMappings,
2173
            function ($assoc) { return $assoc['isCascadePersist']; }
2174
        );
2175
2176 660
        foreach ($associationMappings as $fieldName => $mapping) {
2177 456
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2178
2179 456
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2180 378
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2181 17
                    if ($relatedDocuments->getOwner() !== $document) {
2182 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2183
                    }
2184
                    // Unwrap so that foreach() does not initialize
2185 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2186
                }
2187
2188 378
                $count = 0;
2189 378
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2190 206
                    if ( ! empty($mapping['embedded'])) {
2191 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2192 126
                        if ($knownParent && $knownParent !== $document) {
2193 4
                            $relatedDocument = clone $relatedDocument;
2194 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2195
                        }
2196 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2197 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2198
                    }
2199 378
                    $this->doPersist($relatedDocument, $visited);
2200
                }
2201 364
            } elseif ($relatedDocuments !== null) {
2202 138
                if ( ! empty($mapping['embedded'])) {
2203 77
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2204 77
                    if ($knownParent && $knownParent !== $document) {
2205 6
                        $relatedDocuments = clone $relatedDocuments;
2206 6
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2207
                    }
2208 77
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2209
                }
2210 456
                $this->doPersist($relatedDocuments, $visited);
2211
            }
2212
        }
2213 658
    }
2214
2215
    /**
2216
     * Cascades the delete operation to associated documents.
2217
     *
2218
     * @param object $document
2219
     * @param array $visited
2220
     */
2221 76 View Code Duplication
    private function cascadeRemove($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2222
    {
2223 76
        $class = $this->dm->getClassMetadata(get_class($document));
2224 76
        foreach ($class->fieldMappings as $mapping) {
2225 76
            if ( ! $mapping['isCascadeRemove']) {
2226 75
                continue;
2227
            }
2228 36
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
2229 2
                $document->__load();
2230
            }
2231
2232 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2233 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2234
                // If its a PersistentCollection initialization is intended! No unwrap!
2235 25
                foreach ($relatedDocuments as $relatedDocument) {
2236 25
                    $this->doRemove($relatedDocument, $visited);
2237
                }
2238 24
            } elseif ($relatedDocuments !== null) {
2239 36
                $this->doRemove($relatedDocuments, $visited);
2240
            }
2241
        }
2242 76
    }
2243
2244
    /**
2245
     * Acquire a lock on the given document.
2246
     *
2247
     * @param object $document
2248
     * @param int $lockMode
2249
     * @param int $lockVersion
2250
     * @throws LockException
2251
     * @throws \InvalidArgumentException
2252
     */
2253 9
    public function lock($document, $lockMode, $lockVersion = null)
2254
    {
2255 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2256 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2257
        }
2258
2259 8
        $documentName = get_class($document);
2260 8
        $class = $this->dm->getClassMetadata($documentName);
2261
2262 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2263 3
            if ( ! $class->isVersioned) {
2264 1
                throw LockException::notVersioned($documentName);
2265
            }
2266
2267 2
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2268 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2269 2
                if ($documentVersion != $lockVersion) {
2270 2
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2271
                }
2272
            }
2273 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2274 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2275
        }
2276 6
    }
2277
2278
    /**
2279
     * Releases a lock on the given document.
2280
     *
2281
     * @param object $document
2282
     * @throws \InvalidArgumentException
2283
     */
2284 1
    public function unlock($document)
2285
    {
2286 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2287
            throw new \InvalidArgumentException("Document is not MANAGED.");
2288
        }
2289 1
        $documentName = get_class($document);
2290 1
        $this->getDocumentPersister($documentName)->unlock($document);
2291 1
    }
2292
2293
    /**
2294
     * Clears the UnitOfWork.
2295
     *
2296
     * @param string|null $documentName if given, only documents of this type will get detached.
2297
     */
2298 418
    public function clear($documentName = null)
2299
    {
2300 418
        if ($documentName === null) {
2301 410
            $this->identityMap =
2302 410
            $this->documentIdentifiers =
2303 410
            $this->originalDocumentData =
2304 410
            $this->documentChangeSets =
2305 410
            $this->documentStates =
2306 410
            $this->scheduledForDirtyCheck =
2307 410
            $this->documentInsertions =
2308 410
            $this->documentUpserts =
2309 410
            $this->documentUpdates =
2310 410
            $this->documentDeletions =
2311 410
            $this->collectionUpdates =
2312 410
            $this->collectionDeletions =
2313 410
            $this->parentAssociations =
2314 410
            $this->embeddedDocumentsRegistry =
2315 410
            $this->orphanRemovals =
2316 410
            $this->hasScheduledCollections = array();
2317
        } else {
2318 8
            $visited = array();
2319 8
            foreach ($this->identityMap as $className => $documents) {
2320 8
                if ($className === $documentName) {
2321 5
                    foreach ($documents as $document) {
2322 8
                        $this->doDetach($document, $visited);
2323
                    }
2324
                }
2325
            }
2326
        }
2327
2328 418 View Code Duplication
        if ($this->evm->hasListeners(Events::onClear)) {
0 ignored issues
show
Duplication introduced by
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...
2329
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2330
        }
2331 418
    }
2332
2333
    /**
2334
     * INTERNAL:
2335
     * Schedules an embedded document for removal. The remove() operation will be
2336
     * invoked on that document at the beginning of the next commit of this
2337
     * UnitOfWork.
2338
     *
2339
     * @ignore
2340
     * @param object $document
2341
     */
2342 53
    public function scheduleOrphanRemoval($document)
2343
    {
2344 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2345 53
    }
2346
2347
    /**
2348
     * INTERNAL:
2349
     * Unschedules an embedded or referenced object for removal.
2350
     *
2351
     * @ignore
2352
     * @param object $document
2353
     */
2354 114
    public function unscheduleOrphanRemoval($document)
2355
    {
2356 114
        $oid = spl_object_hash($document);
2357 114
        if (isset($this->orphanRemovals[$oid])) {
2358 1
            unset($this->orphanRemovals[$oid]);
2359
        }
2360 114
    }
2361
2362
    /**
2363
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2364
     *  1) sets owner if it was cloned
2365
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2366
     *  3) NOP if state is OK
2367
     * Returned collection should be used from now on (only important with 2nd point)
2368
     *
2369
     * @param PersistentCollectionInterface $coll
2370
     * @param object $document
2371
     * @param ClassMetadata $class
2372
     * @param string $propName
2373
     * @return PersistentCollectionInterface
2374
     */
2375 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2376
    {
2377 8
        $owner = $coll->getOwner();
2378 8
        if ($owner === null) { // cloned
2379 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2380 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2381 2
            if ( ! $coll->isInitialized()) {
2382 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2383
            }
2384 2
            $newValue = clone $coll;
2385 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2386 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2387 2
            if ($this->isScheduledForUpdate($document)) {
2388
                // @todo following line should be superfluous once collections are stored in change sets
2389
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2390
            }
2391 2
            return $newValue;
2392
        }
2393 6
        return $coll;
2394
    }
2395
2396
    /**
2397
     * INTERNAL:
2398
     * Schedules a complete collection for removal when this UnitOfWork commits.
2399
     *
2400
     * @param PersistentCollectionInterface $coll
2401
     */
2402 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2403
    {
2404 43
        $oid = spl_object_hash($coll);
2405 43
        unset($this->collectionUpdates[$oid]);
2406 43
        if ( ! isset($this->collectionDeletions[$oid])) {
2407 43
            $this->collectionDeletions[$oid] = $coll;
2408 43
            $this->scheduleCollectionOwner($coll);
2409
        }
2410 43
    }
2411
2412
    /**
2413
     * Checks whether a PersistentCollection is scheduled for deletion.
2414
     *
2415
     * @param PersistentCollectionInterface $coll
2416
     * @return boolean
2417
     */
2418 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2419
    {
2420 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2421
    }
2422
2423
    /**
2424
     * INTERNAL:
2425
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2426
     *
2427
     * @param PersistentCollectionInterface $coll
2428
     */
2429 232 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2430
    {
2431 232
        $oid = spl_object_hash($coll);
2432 232
        if (isset($this->collectionDeletions[$oid])) {
2433 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2434 12
            unset($this->collectionDeletions[$oid]);
2435 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2436
        }
2437 232
    }
2438
2439
    /**
2440
     * INTERNAL:
2441
     * Schedules a collection for update when this UnitOfWork commits.
2442
     *
2443
     * @param PersistentCollectionInterface $coll
2444
     */
2445 254
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2446
    {
2447 254
        $mapping = $coll->getMapping();
2448 254
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2449
            /* There is no need to $unset collection if it will be $set later
2450
             * This is NOP if collection is not scheduled for deletion
2451
             */
2452 41
            $this->unscheduleCollectionDeletion($coll);
2453
        }
2454 254
        $oid = spl_object_hash($coll);
2455 254
        if ( ! isset($this->collectionUpdates[$oid])) {
2456 254
            $this->collectionUpdates[$oid] = $coll;
2457 254
            $this->scheduleCollectionOwner($coll);
2458
        }
2459 254
    }
2460
2461
    /**
2462
     * INTERNAL:
2463
     * Unschedules a collection from being updated when this UnitOfWork commits.
2464
     *
2465
     * @param PersistentCollectionInterface $coll
2466
     */
2467 232 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
2468
    {
2469 232
        $oid = spl_object_hash($coll);
2470 232
        if (isset($this->collectionUpdates[$oid])) {
2471 222
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2472 222
            unset($this->collectionUpdates[$oid]);
2473 222
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2474
        }
2475 232
    }
2476
2477
    /**
2478
     * Checks whether a PersistentCollection is scheduled for update.
2479
     *
2480
     * @param PersistentCollectionInterface $coll
2481
     * @return boolean
2482
     */
2483 133
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2484
    {
2485 133
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2486
    }
2487
2488
    /**
2489
     * INTERNAL:
2490
     * Gets PersistentCollections that have been visited during computing change
2491
     * set of $document
2492
     *
2493
     * @param object $document
2494
     * @return PersistentCollectionInterface[]
2495
     */
2496 611
    public function getVisitedCollections($document)
2497
    {
2498 611
        $oid = spl_object_hash($document);
2499 611
        return isset($this->visitedCollections[$oid])
2500 257
                ? $this->visitedCollections[$oid]
2501 611
                : array();
2502
    }
2503
2504
    /**
2505
     * INTERNAL:
2506
     * Gets PersistentCollections that are scheduled to update and related to $document
2507
     *
2508
     * @param object $document
2509
     * @return array
2510
     */
2511 612
    public function getScheduledCollections($document)
2512
    {
2513 612
        $oid = spl_object_hash($document);
2514 612
        return isset($this->hasScheduledCollections[$oid])
2515 255
                ? $this->hasScheduledCollections[$oid]
2516 612
                : array();
2517
    }
2518
2519
    /**
2520
     * Checks whether the document is related to a PersistentCollection
2521
     * scheduled for update or deletion.
2522
     *
2523
     * @param object $document
2524
     * @return boolean
2525
     */
2526 52
    public function hasScheduledCollections($document)
2527
    {
2528 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2529
    }
2530
2531
    /**
2532
     * Marks the PersistentCollection's top-level owner as having a relation to
2533
     * a collection scheduled for update or deletion.
2534
     *
2535
     * If the owner is not scheduled for any lifecycle action, it will be
2536
     * scheduled for update to ensure that versioning takes place if necessary.
2537
     *
2538
     * If the collection is nested within atomic collection, it is immediately
2539
     * unscheduled and atomic one is scheduled for update instead. This makes
2540
     * calculating update data way easier.
2541
     *
2542
     * @param PersistentCollectionInterface $coll
2543
     */
2544 256
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2545
    {
2546 256
        $document = $this->getOwningDocument($coll->getOwner());
2547 256
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2548
2549 256
        if ($document !== $coll->getOwner()) {
2550 25
            $parent = $coll->getOwner();
2551 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2552 25
                list($mapping, $parent, ) = $parentAssoc;
2553
            }
2554 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2555 8
                $class = $this->dm->getClassMetadata(get_class($document));
2556 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
0 ignored issues
show
Bug introduced by
The variable $mapping does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2557 8
                $this->scheduleCollectionUpdate($atomicCollection);
2558 8
                $this->unscheduleCollectionDeletion($coll);
2559 8
                $this->unscheduleCollectionUpdate($coll);
2560
            }
2561
        }
2562
2563 256
        if ( ! $this->isDocumentScheduled($document)) {
2564 50
            $this->scheduleForUpdate($document);
2565
        }
2566 256
    }
2567
2568
    /**
2569
     * Get the top-most owning document of a given document
2570
     *
2571
     * If a top-level document is provided, that same document will be returned.
2572
     * For an embedded document, we will walk through parent associations until
2573
     * we find a top-level document.
2574
     *
2575
     * @param object $document
2576
     * @throws \UnexpectedValueException when a top-level document could not be found
2577
     * @return object
2578
     */
2579 258
    public function getOwningDocument($document)
2580
    {
2581 258
        $class = $this->dm->getClassMetadata(get_class($document));
2582 258
        while ($class->isEmbeddedDocument) {
2583 40
            $parentAssociation = $this->getParentAssociation($document);
2584
2585 40
            if ( ! $parentAssociation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentAssociation of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2586
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2587
            }
2588
2589 40
            list(, $document, ) = $parentAssociation;
2590 40
            $class = $this->dm->getClassMetadata(get_class($document));
2591
        }
2592
2593 258
        return $document;
2594
    }
2595
2596
    /**
2597
     * Gets the class name for an association (embed or reference) with respect
2598
     * to any discriminator value.
2599
     *
2600
     * @param array      $mapping Field mapping for the association
2601
     * @param array|null $data    Data for the embedded document or reference
2602
     * @return string Class name.
2603
     */
2604 228
    public function getClassNameForAssociation(array $mapping, $data)
2605
    {
2606 228
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2607
2608 228
        $discriminatorValue = null;
2609 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2610 21
            $discriminatorValue = $data[$discriminatorField];
2611 208
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2612
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2613
        }
2614
2615 228
        if ($discriminatorValue !== null) {
2616 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2617 10
                ? $mapping['discriminatorMap'][$discriminatorValue]
2618 21
                : $discriminatorValue;
2619
        }
2620
2621 208
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2622
2623 208 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
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...
2624 15
            $discriminatorValue = $data[$class->discriminatorField];
2625 193
        } elseif ($class->defaultDiscriminatorValue !== null) {
2626 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2627
        }
2628
2629 208
        if ($discriminatorValue !== null) {
2630 16
            return isset($class->discriminatorMap[$discriminatorValue])
2631 14
                ? $class->discriminatorMap[$discriminatorValue]
2632 16
                : $discriminatorValue;
2633
        }
2634
2635 192
        return $mapping['targetDocument'];
2636
    }
2637
2638
    /**
2639
     * INTERNAL:
2640
     * Creates a document. Used for reconstitution of documents during hydration.
2641
     *
2642
     * @ignore
2643
     * @param string $className The name of the document class.
2644
     * @param array $data The data for the document.
2645
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2646
     * @param object $document The document to be hydrated into in case of creation
2647
     * @return object The document instance.
2648
     * @internal Highly performance-sensitive method.
2649
     */
2650 422
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2651
    {
2652 422
        $class = $this->dm->getClassMetadata($className);
2653
2654
        // @TODO figure out how to remove this
2655 422
        $discriminatorValue = null;
2656 422 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
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...
2657 19
            $discriminatorValue = $data[$class->discriminatorField];
2658 414
        } elseif (isset($class->defaultDiscriminatorValue)) {
2659 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2660
        }
2661
2662 422
        if ($discriminatorValue !== null) {
2663 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2664 18
                ? $class->discriminatorMap[$discriminatorValue]
2665 20
                : $discriminatorValue;
2666
2667 20
            $class = $this->dm->getClassMetadata($className);
2668
2669 20
            unset($data[$class->discriminatorField]);
2670
        }
2671
        
2672 422
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2673 2
            $document = $class->newInstance();
2674 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2675 2
            return $document;
2676
        }
2677
2678 421
        $isManagedObject = false;
2679 421
        if (! $class->isQueryResultDocument) {
2680 421
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2681 421
            $serializedId = serialize($id);
2682 421
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2683
        }
2684
2685 421
        if ($isManagedObject) {
2686 110
            $document = $this->identityMap[$class->name][$serializedId];
0 ignored issues
show
Bug introduced by
The variable $serializedId does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2687 110
            $oid = spl_object_hash($document);
2688 110
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
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...
2689 14
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
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...
2690 14
                $overrideLocalValues = true;
2691 14
                if ($document instanceof NotifyPropertyChanged) {
2692 14
                    $document->addPropertyChangedListener($this);
2693
                }
2694
            } else {
2695 106
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2696
            }
2697 110
            if ($overrideLocalValues) {
2698 52
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2699 110
                $this->originalDocumentData[$oid] = $data;
2700
            }
2701
        } else {
2702 380
            if ($document === null) {
2703 380
                $document = $class->newInstance();
2704
            }
2705
2706 380
            if (! $class->isQueryResultDocument) {
2707 379
                $this->registerManaged($document, $id, $data);
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2708 379
                $oid = spl_object_hash($document);
2709 379
                $this->documentStates[$oid] = self::STATE_MANAGED;
2710 379
                $this->identityMap[$class->name][$serializedId] = $document;
2711
            }
2712
2713 380
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2714
2715 380
            if (! $class->isQueryResultDocument) {
2716 379
                $this->originalDocumentData[$oid] = $data;
0 ignored issues
show
Bug introduced by
The variable $oid does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2717
            }
2718
        }
2719
2720 421
        return $document;
2721
    }
2722
2723
    /**
2724
     * Initializes (loads) an uninitialized persistent collection of a document.
2725
     *
2726
     * @param PersistentCollectionInterface $collection The collection to initialize.
2727
     */
2728 173
    public function loadCollection(PersistentCollectionInterface $collection)
2729
    {
2730 173
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2731 172
        $this->lifecycleEventManager->postCollectionLoad($collection);
2732 172
    }
2733
2734
    /**
2735
     * Gets the identity map of the UnitOfWork.
2736
     *
2737
     * @return array
2738
     */
2739
    public function getIdentityMap()
2740
    {
2741
        return $this->identityMap;
2742
    }
2743
2744
    /**
2745
     * Gets the original data of a document. The original data is the data that was
2746
     * present at the time the document was reconstituted from the database.
2747
     *
2748
     * @param object $document
2749
     * @return array
2750
     */
2751 1
    public function getOriginalDocumentData($document)
2752
    {
2753 1
        $oid = spl_object_hash($document);
2754 1
        if (isset($this->originalDocumentData[$oid])) {
2755 1
            return $this->originalDocumentData[$oid];
2756
        }
2757
        return array();
2758
    }
2759
2760
    /**
2761
     * @ignore
2762
     */
2763 56
    public function setOriginalDocumentData($document, array $data)
2764
    {
2765 56
        $oid = spl_object_hash($document);
2766 56
        $this->originalDocumentData[$oid] = $data;
2767 56
        unset($this->documentChangeSets[$oid]);
2768 56
    }
2769
2770
    /**
2771
     * INTERNAL:
2772
     * Sets a property value of the original data array of a document.
2773
     *
2774
     * @ignore
2775
     * @param string $oid
2776
     * @param string $property
2777
     * @param mixed $value
2778
     */
2779 6
    public function setOriginalDocumentProperty($oid, $property, $value)
2780
    {
2781 6
        $this->originalDocumentData[$oid][$property] = $value;
2782 6
    }
2783
2784
    /**
2785
     * Gets the identifier of a document.
2786
     *
2787
     * @param object $document
2788
     * @return mixed The identifier value
2789
     */
2790 457
    public function getDocumentIdentifier($document)
2791
    {
2792 457
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2793 457
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2794
    }
2795
2796
    /**
2797
     * Checks whether the UnitOfWork has any pending insertions.
2798
     *
2799
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2800
     */
2801
    public function hasPendingInsertions()
2802
    {
2803
        return ! empty($this->documentInsertions);
2804
    }
2805
2806
    /**
2807
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2808
     * number of documents in the identity map.
2809
     *
2810
     * @return integer
2811
     */
2812 2
    public function size()
2813
    {
2814 2
        $count = 0;
2815 2
        foreach ($this->identityMap as $documentSet) {
2816 2
            $count += count($documentSet);
2817
        }
2818 2
        return $count;
2819
    }
2820
2821
    /**
2822
     * INTERNAL:
2823
     * Registers a document as managed.
2824
     *
2825
     * TODO: This method assumes that $id is a valid PHP identifier for the
2826
     * document class. If the class expects its database identifier to be a
2827
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2828
     * document identifiers map will become inconsistent with the identity map.
2829
     * In the future, we may want to round-trip $id through a PHP and database
2830
     * conversion and throw an exception if it's inconsistent.
2831
     *
2832
     * @param object $document The document.
2833
     * @param array $id The identifier values.
2834
     * @param array $data The original document data.
2835
     */
2836 404
    public function registerManaged($document, $id, array $data)
2837
    {
2838 404
        $oid = spl_object_hash($document);
2839 404
        $class = $this->dm->getClassMetadata(get_class($document));
2840
2841 404
        if ( ! $class->identifier || $id === null) {
2842 109
            $this->documentIdentifiers[$oid] = $oid;
2843
        } else {
2844 398
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2845
        }
2846
2847 404
        $this->documentStates[$oid] = self::STATE_MANAGED;
2848 404
        $this->originalDocumentData[$oid] = $data;
2849 404
        $this->addToIdentityMap($document);
2850 404
    }
2851
2852
    /**
2853
     * INTERNAL:
2854
     * Clears the property changeset of the document with the given OID.
2855
     *
2856
     * @param string $oid The document's OID.
2857
     */
2858 1
    public function clearDocumentChangeSet($oid)
2859
    {
2860 1
        $this->documentChangeSets[$oid] = array();
2861 1
    }
2862
2863
    /* PropertyChangedListener implementation */
2864
2865
    /**
2866
     * Notifies this UnitOfWork of a property change in a document.
2867
     *
2868
     * @param object $document The document that owns the property.
2869
     * @param string $propertyName The name of the property that changed.
2870
     * @param mixed $oldValue The old value of the property.
2871
     * @param mixed $newValue The new value of the property.
2872
     */
2873 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2874
    {
2875 2
        $oid = spl_object_hash($document);
2876 2
        $class = $this->dm->getClassMetadata(get_class($document));
2877
2878 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2879 1
            return; // ignore non-persistent fields
2880
        }
2881
2882
        // Update changeset and mark document for synchronization
2883 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2884 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2885 2
            $this->scheduleForDirtyCheck($document);
2886
        }
2887 2
    }
2888
2889
    /**
2890
     * Gets the currently scheduled document insertions in this UnitOfWork.
2891
     *
2892
     * @return array
2893
     */
2894 5
    public function getScheduledDocumentInsertions()
2895
    {
2896 5
        return $this->documentInsertions;
2897
    }
2898
2899
    /**
2900
     * Gets the currently scheduled document upserts in this UnitOfWork.
2901
     *
2902
     * @return array
2903
     */
2904 3
    public function getScheduledDocumentUpserts()
2905
    {
2906 3
        return $this->documentUpserts;
2907
    }
2908
2909
    /**
2910
     * Gets the currently scheduled document updates in this UnitOfWork.
2911
     *
2912
     * @return array
2913
     */
2914 3
    public function getScheduledDocumentUpdates()
2915
    {
2916 3
        return $this->documentUpdates;
2917
    }
2918
2919
    /**
2920
     * Gets the currently scheduled document deletions in this UnitOfWork.
2921
     *
2922
     * @return array
2923
     */
2924
    public function getScheduledDocumentDeletions()
2925
    {
2926
        return $this->documentDeletions;
2927
    }
2928
2929
    /**
2930
     * Get the currently scheduled complete collection deletions
2931
     *
2932
     * @return array
2933
     */
2934
    public function getScheduledCollectionDeletions()
2935
    {
2936
        return $this->collectionDeletions;
2937
    }
2938
2939
    /**
2940
     * Gets the currently scheduled collection inserts, updates and deletes.
2941
     *
2942
     * @return array
2943
     */
2944
    public function getScheduledCollectionUpdates()
2945
    {
2946
        return $this->collectionUpdates;
2947
    }
2948
2949
    /**
2950
     * Helper method to initialize a lazy loading proxy or persistent collection.
2951
     *
2952
     * @param object
2953
     * @return void
2954
     */
2955
    public function initializeObject($obj)
2956
    {
2957
        if ($obj instanceof Proxy) {
2958
            $obj->__load();
2959
        } elseif ($obj instanceof PersistentCollectionInterface) {
2960
            $obj->initialize();
2961
        }
2962
    }
2963
2964 1
    private function objToStr($obj)
2965
    {
2966 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2967
    }
2968
}
2969