Completed
Push — master ( 8a5bc1...66dea1 )
by Andreas
23:33
created

UnitOfWork::getOrCreateDocument()   C

Complexity

Conditions 14
Paths 174

Size

Total Lines 70
Code Lines 45

Duplication

Lines 5
Ratio 7.14 %

Code Coverage

Tests 41
CRAP Score 14

Importance

Changes 0
Metric Value
dl 5
loc 70
ccs 41
cts 41
cp 1
rs 5.1756
c 0
b 0
f 0
cc 14
eloc 45
nc 174
nop 4
crap 14

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Doctrine\ODM\MongoDB;
4
5
use Doctrine\Common\Collections\ArrayCollection;
6
use Doctrine\Common\Collections\Collection;
7
use Doctrine\Common\EventManager;
8
use Doctrine\Common\NotifyPropertyChanged;
9
use Doctrine\Common\PropertyChangedListener;
10
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
11
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
12
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
13
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
14
use Doctrine\ODM\MongoDB\Proxy\Proxy;
15
use Doctrine\ODM\MongoDB\Query\Query;
16
use Doctrine\ODM\MongoDB\Types\Type;
17
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
18
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
19
20
/**
21
 * The UnitOfWork is responsible for tracking changes to objects during an
22
 * "object-level" transaction and for writing out changes to the database
23
 * in the correct order.
24
 *
25
 * @since       1.0
26
 */
27
class UnitOfWork implements PropertyChangedListener
28
{
29
    /**
30
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
31
     */
32
    const STATE_MANAGED = 1;
33
34
    /**
35
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
36
     * and is not (yet) managed by a DocumentManager.
37
     */
38
    const STATE_NEW = 2;
39
40
    /**
41
     * A detached document is an instance with a persistent identity that is not
42
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
43
     */
44
    const STATE_DETACHED = 3;
45
46
    /**
47
     * A removed document instance is an instance with a persistent identity,
48
     * associated with a DocumentManager, whose persistent state has been
49
     * deleted (or is scheduled for deletion).
50
     */
51
    const STATE_REMOVED = 4;
52
53
    /**
54
     * The identity map holds references to all managed documents.
55
     *
56
     * Documents are grouped by their class name, and then indexed by the
57
     * serialized string of their database identifier field or, if the class
58
     * has no identifier, the SPL object hash. Serializing the identifier allows
59
     * differentiation of values that may be equal (via type juggling) but not
60
     * identical.
61
     *
62
     * Since all classes in a hierarchy must share the same identifier set,
63
     * we always take the root class name of the hierarchy.
64
     *
65
     * @var array
66
     */
67
    private $identityMap = array();
68
69
    /**
70
     * Map of all identifiers of managed documents.
71
     * Keys are object ids (spl_object_hash).
72
     *
73
     * @var array
74
     */
75
    private $documentIdentifiers = array();
76
77
    /**
78
     * Map of the original document data of managed documents.
79
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
80
     * at commit time.
81
     *
82
     * @var array
83
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
84
     *           A value will only really be copied if the value in the document is modified
85
     *           by the user.
86
     */
87
    private $originalDocumentData = array();
88
89
    /**
90
     * Map of document changes. Keys are object ids (spl_object_hash).
91
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
92
     *
93
     * @var array
94
     */
95
    private $documentChangeSets = array();
96
97
    /**
98
     * The (cached) states of any known documents.
99
     * Keys are object ids (spl_object_hash).
100
     *
101
     * @var array
102
     */
103
    private $documentStates = array();
104
105
    /**
106
     * Map of documents that are scheduled for dirty checking at commit time.
107
     *
108
     * Documents are grouped by their class name, and then indexed by their SPL
109
     * object hash. This is only used for documents with a change tracking
110
     * policy of DEFERRED_EXPLICIT.
111
     *
112
     * @var array
113
     * @todo rename: scheduledForSynchronization
114
     */
115
    private $scheduledForDirtyCheck = array();
116
117
    /**
118
     * A list of all pending document insertions.
119
     *
120
     * @var array
121
     */
122
    private $documentInsertions = array();
123
124
    /**
125
     * A list of all pending document updates.
126
     *
127
     * @var array
128
     */
129
    private $documentUpdates = array();
130
131
    /**
132
     * A list of all pending document upserts.
133
     *
134
     * @var array
135
     */
136
    private $documentUpserts = array();
137
138
    /**
139
     * A list of all pending document deletions.
140
     *
141
     * @var array
142
     */
143
    private $documentDeletions = array();
144
145
    /**
146
     * All pending collection deletions.
147
     *
148
     * @var array
149
     */
150
    private $collectionDeletions = array();
151
152
    /**
153
     * All pending collection updates.
154
     *
155
     * @var array
156
     */
157
    private $collectionUpdates = array();
158
159
    /**
160
     * A list of documents related to collections scheduled for update or deletion
161
     *
162
     * @var array
163
     */
164
    private $hasScheduledCollections = array();
165
166
    /**
167
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
168
     * At the end of the UnitOfWork all these collections will make new snapshots
169
     * of their data.
170
     *
171
     * @var array
172
     */
173
    private $visitedCollections = array();
174
175
    /**
176
     * The DocumentManager that "owns" this UnitOfWork instance.
177
     *
178
     * @var DocumentManager
179
     */
180
    private $dm;
181
182
    /**
183
     * The EventManager used for dispatching events.
184
     *
185
     * @var EventManager
186
     */
187
    private $evm;
188
189
    /**
190
     * Additional documents that are scheduled for removal.
191
     *
192
     * @var array
193
     */
194
    private $orphanRemovals = array();
195
196
    /**
197
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
198
     *
199
     * @var HydratorFactory
200
     */
201
    private $hydratorFactory;
202
203
    /**
204
     * The document persister instances used to persist document instances.
205
     *
206
     * @var array
207
     */
208
    private $persisters = array();
209
210
    /**
211
     * The collection persister instance used to persist changes to collections.
212
     *
213
     * @var Persisters\CollectionPersister
214
     */
215
    private $collectionPersister;
216
217
    /**
218
     * The persistence builder instance used in DocumentPersisters.
219
     *
220
     * @var PersistenceBuilder
221
     */
222
    private $persistenceBuilder;
223
224
    /**
225
     * Array of parent associations between embedded documents.
226
     *
227
     * @var array
228
     */
229
    private $parentAssociations = array();
230
231
    /**
232
     * @var LifecycleEventManager
233
     */
234
    private $lifecycleEventManager;
235
236
    /**
237
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
238
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
239
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
240
     *
241
     * @var array
242
     */
243
    private $embeddedDocumentsRegistry = array();
244
245
    /**
246
     * @var int
247
     */
248
    private $commitsInProgress = 0;
249
250
    /**
251
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
252
     *
253
     * @param DocumentManager $dm
254
     * @param EventManager $evm
255
     * @param HydratorFactory $hydratorFactory
256
     */
257 1638
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
258
    {
259 1638
        $this->dm = $dm;
260 1638
        $this->evm = $evm;
261 1638
        $this->hydratorFactory = $hydratorFactory;
262 1638
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
263 1638
    }
264
265
    /**
266
     * Factory for returning new PersistenceBuilder instances used for preparing data into
267
     * queries for insert persistence.
268
     *
269
     * @return PersistenceBuilder $pb
270
     */
271 1086
    public function getPersistenceBuilder()
272
    {
273 1086
        if ( ! $this->persistenceBuilder) {
274 1086
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
275
        }
276 1086
        return $this->persistenceBuilder;
277
    }
278
279
    /**
280
     * Sets the parent association for a given embedded document.
281
     *
282
     * @param object $document
283
     * @param array $mapping
284
     * @param object $parent
285
     * @param string $propertyPath
286
     */
287 177
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
288
    {
289 177
        $oid = spl_object_hash($document);
290 177
        $this->embeddedDocumentsRegistry[$oid] = $document;
291 177
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
292 177
    }
293
294
    /**
295
     * Gets the parent association for a given embedded document.
296
     *
297
     *     <code>
298
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
299
     *     </code>
300
     *
301
     * @param object $document
302
     * @return array $association
303
     */
304 203
    public function getParentAssociation($document)
305
    {
306 203
        $oid = spl_object_hash($document);
307 203
308 197
        return $this->parentAssociations[$oid] ?? null;
309
    }
310 152
311
    /**
312
     * Get the document persister instance for the given document name
313
     *
314
     * @param string $documentName
315
     * @return Persisters\DocumentPersister
316
     */
317
    public function getDocumentPersister($documentName)
318
    {
319 1084
        if ( ! isset($this->persisters[$documentName])) {
320
            $class = $this->dm->getClassMetadata($documentName);
321 1084
            $pb = $this->getPersistenceBuilder();
322 1071
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
323 1071
        }
324 1071
        return $this->persisters[$documentName];
325
    }
326 1084
327
    /**
328
     * Get the collection persister instance.
329
     *
330
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
331
     */
332
    public function getCollectionPersister()
333
    {
334 1084
        if ( ! isset($this->collectionPersister)) {
335
            $pb = $this->getPersistenceBuilder();
336 1084
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
337 1084
        }
338 1084
        return $this->collectionPersister;
339
    }
340 1084
341
    /**
342
     * Set the document persister instance to use for the given document name
343
     *
344
     * @param string $documentName
345
     * @param Persisters\DocumentPersister $persister
346
     */
347
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
348
    {
349 13
        $this->persisters[$documentName] = $persister;
350
    }
351 13
352 13
    /**
353
     * Commits the UnitOfWork, executing all operations that have been postponed
354
     * up to this point. The state of all managed documents will be synchronized with
355
     * the database.
356
     *
357
     * The operations are executed in the following order:
358
     *
359
     * 1) All document insertions
360
     * 2) All document updates
361
     * 3) All document deletions
362
     *
363
     * @param object $document
364
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
365
     */
366
    public function commit($document = null, array $options = array())
367
    {
368 577
        // Raise preFlush
369
        if ($this->evm->hasListeners(Events::preFlush)) {
370
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
371 577
        }
372
373
        // Compute changes done since last commit.
374
        if ($document === null) {
375
            $this->computeChangeSets();
376 577
        } elseif (is_object($document)) {
377 572
            $this->computeSingleDocumentChangeSet($document);
378 13
        } elseif (is_array($document)) {
379 12
            foreach ($document as $object) {
380 1
                $this->computeSingleDocumentChangeSet($object);
381 1
            }
382 1
        }
383
384
        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...
385
            $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...
386 575
            $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...
387 245
            $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...
388 202
            $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...
389 188
            $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...
390 22
            $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...
391 22
        ) {
392 575
            return; // Nothing to do.
393
        }
394 22
395
        $this->commitsInProgress++;
396
        if ($this->commitsInProgress > 1) {
397 572
            @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...
398 572
        }
399
        try {
400
            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...
401
                foreach ($this->orphanRemovals as $removal) {
402 572
                    $this->remove($removal);
403 44
                }
404 44
            }
405
406
            // Raise onFlush
407
            if ($this->evm->hasListeners(Events::onFlush)) {
408
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
409 572
            }
410 4
411
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
412
                list($class, $documents) = $classAndDocuments;
413 571
                $this->executeUpserts($class, $documents, $options);
414 86
            }
415 86
416
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
417
                list($class, $documents) = $classAndDocuments;
418 571
                $this->executeInserts($class, $documents, $options);
419 496
            }
420 496
421
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
422
                list($class, $documents) = $classAndDocuments;
423 570
                $this->executeUpdates($class, $documents, $options);
424 208
            }
425 208
426
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
427
                list($class, $documents) = $classAndDocuments;
428 570
                $this->executeDeletions($class, $documents, $options);
429 65
            }
430 65
431
            // Raise postFlush
432
            if ($this->evm->hasListeners(Events::postFlush)) {
433
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
434 570
            }
435
436
            // Clear up
437
            $this->documentInsertions =
438
            $this->documentUpserts =
439 570
            $this->documentUpdates =
440 570
            $this->documentDeletions =
441 570
            $this->documentChangeSets =
442 570
            $this->collectionUpdates =
443 570
            $this->collectionDeletions =
444 570
            $this->visitedCollections =
445 570
            $this->scheduledForDirtyCheck =
446 570
            $this->orphanRemovals =
447 570
            $this->hasScheduledCollections = array();
448 570
        } finally {
449 570
            $this->commitsInProgress--;
450 570
        }
451 572
    }
452
453 570
    /**
454
     * Groups a list of scheduled documents by their class.
455
     *
456
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
457
     * @param bool $includeEmbedded
458
     * @return array Tuples of ClassMetadata and a corresponding array of objects
459
     */
460
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
461
    {
462 571
        if (empty($documents)) {
463
            return array();
464 571
        }
465 571
        $divided = array();
466
        $embeds = array();
467 570
        foreach ($documents as $oid => $d) {
468 570
            $className = get_class($d);
469 570
            if (isset($embeds[$className])) {
470 570
                continue;
471 570
            }
472 69
            if (isset($divided[$className])) {
473
                $divided[$className][1][$oid] = $d;
474 570
                continue;
475 158
            }
476 158
            $class = $this->dm->getClassMetadata($className);
477
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
478 570
                $embeds[$className] = true;
479 570
                continue;
480 154
            }
481 154
            if (empty($divided[$class->name])) {
482
                $divided[$class->name] = array($class, array($oid => $d));
483 570
            } else {
484 570
                $divided[$class->name][1][$oid] = $d;
485
            }
486 570
        }
487
        return $divided;
488
    }
489 570
490
    /**
491
     * Compute changesets of all documents scheduled for insertion.
492
     *
493
     * Embedded documents will not be processed.
494
     */
495 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...
496
    {
497 579
        foreach ($this->documentInsertions as $document) {
498
            $class = $this->dm->getClassMetadata(get_class($document));
499 579
            if ( ! $class->isEmbeddedDocument) {
500 507
                $this->computeChangeSet($class, $document);
501 507
            }
502 507
        }
503
    }
504
505 578
    /**
506
     * Compute changesets of all documents scheduled for upsert.
507
     *
508
     * Embedded documents will not be processed.
509
     */
510 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...
511
    {
512 578
        foreach ($this->documentUpserts as $document) {
513
            $class = $this->dm->getClassMetadata(get_class($document));
514 578
            if ( ! $class->isEmbeddedDocument) {
515 85
                $this->computeChangeSet($class, $document);
516 85
            }
517 85
        }
518
    }
519
520 578
    /**
521
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
522
     *
523
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
524
     * 2. Proxies are skipped.
525
     * 3. Only if document is properly managed.
526
     *
527
     * @param  object $document
528
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
529
     * @return void
530
     */
531
    private function computeSingleDocumentChangeSet($document)
532
    {
533 13
        $state = $this->getDocumentState($document);
534
535 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
536
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
537 13
        }
538 1
539
        $class = $this->dm->getClassMetadata(get_class($document));
540
541 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
542
            $this->persist($document);
543 12
        }
544 10
545
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
546
        $this->computeScheduleInsertsChangeSets();
547
        $this->computeScheduleUpsertsChangeSets();
548 12
549 12
        // Ignore uninitialized proxy objects
550
        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...
551
            return;
552 12
        }
553
554
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
555
        $oid = spl_object_hash($document);
556
557 12 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...
558
            && ! isset($this->documentUpserts[$oid])
559 12
            && ! isset($this->documentDeletions[$oid])
560 12
            && isset($this->documentStates[$oid])
561 12
        ) {
562 12
            $this->computeChangeSet($class, $document);
563
        }
564 7
    }
565
566 12
    /**
567
     * Gets the changeset for a document.
568
     *
569
     * @param object $document
570
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
571
     */
572
    public function getDocumentChangeSet($document)
573
    {
574 573
        $oid = spl_object_hash($document);
575
576 573
        return $this->documentChangeSets[$oid] ?? array();
577 573
    }
578 569
579
    /**
580 55
     * INTERNAL:
581
     * Sets the changeset for a document.
582
     *
583
     * @param object $document
584
     * @param array $changeset
585
     */
586
    public function setDocumentChangeSet($document, $changeset)
587
    {
588
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
589
    }
590 1
591
    /**
592 1
     * Get a documents actual data, flattening all the objects to arrays.
593 1
     *
594
     * @param object $document
595
     * @return array
596
     */
597
    public function getDocumentActualData($document)
598
    {
599
        $class = $this->dm->getClassMetadata(get_class($document));
600
        $actualData = array();
601 580
        foreach ($class->reflFields as $name => $refProp) {
602
            $mapping = $class->fieldMappings[$name];
603 580
            // skip not saved fields
604 580
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
605 580
                continue;
606 580
            }
607
            $value = $refProp->getValue($document);
608 580
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
609 27
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
610
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
611 580
                if ( ! $value instanceof Collection) {
612 580
                    $value = new ArrayCollection($value);
613 580
                }
614
615 371
                // Inject PersistentCollection
616 139
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
617
                $coll->setOwner($document, $mapping);
618
                $coll->setDirty( ! $value->isEmpty());
619
                $class->reflFields[$name]->setValue($document, $coll);
620 371
                $actualData[$name] = $coll;
621 371
            } else {
622 371
                $actualData[$name] = $value;
623 371
            }
624 371
        }
625
        return $actualData;
626 580
    }
627
628
    /**
629 580
     * Computes the changes that happened to a single document.
630
     *
631
     * Modifies/populates the following properties:
632
     *
633
     * {@link originalDocumentData}
634
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
635
     * then it was not fetched from the database and therefore we have no original
636
     * document data yet. All of the current document data is stored as the original document data.
637
     *
638
     * {@link documentChangeSets}
639
     * The changes detected on all properties of the document are stored there.
640
     * A change is a tuple array where the first entry is the old value and the second
641
     * entry is the new value of the property. Changesets are used by persisters
642
     * to INSERT/UPDATE the persistent document state.
643
     *
644
     * {@link documentUpdates}
645
     * If the document is already fully MANAGED (has been fetched from the database before)
646
     * and any changes to its properties are detected, then a reference to the document is stored
647
     * there to mark it for an update.
648
     *
649
     * @param ClassMetadata $class The class descriptor of the document.
650
     * @param object $document The document for which to compute the changes.
651
     */
652
    public function computeChangeSet(ClassMetadata $class, $document)
653
    {
654
        if ( ! $class->isInheritanceTypeNone()) {
655
            $class = $this->dm->getClassMetadata(get_class($document));
656 576
        }
657
658 576
        // Fire PreFlush lifecycle callbacks
659 175 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...
660
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
661
        }
662
663 576
        $this->computeOrRecomputeChangeSet($class, $document);
664 11
    }
665
666
    /**
667 576
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
668 575
     *
669
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
670
     * @param object $document
671
     * @param boolean $recompute
672
     */
673
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
674
    {
675
        $oid = spl_object_hash($document);
676
        $actualData = $this->getDocumentActualData($document);
677 576
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
678
        if ($isNewDocument) {
679 576
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
680 576
            // These result in an INSERT.
681 576
            $this->originalDocumentData[$oid] = $actualData;
682 576
            $changeSet = array();
683
            foreach ($actualData as $propName => $actualValue) {
684
                /* At this PersistentCollection shouldn't be here, probably it
685 576
                 * was cloned and its ownership must be fixed
686 576
                 */
687 576
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
688
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
689
                    $actualValue = $actualData[$propName];
690
                }
691 576
                // ignore inverse side of reference relationship
692 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...
693
                    continue;
694
                }
695
                $changeSet[$propName] = array(null, $actualValue);
696 576
            }
697 184
            $this->documentChangeSets[$oid] = $changeSet;
698
        } else {
699 576
            if ($class->isReadOnly) {
700
                return;
701 576
            }
702
            // Document is "fully" MANAGED: it was already fully persisted before
703 264
            // and we have a copy of the original data
704 2
            $originalData = $this->originalDocumentData[$oid];
705
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
706
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
707
                $changeSet = $this->documentChangeSets[$oid];
708 262
            } else {
709 262
                $changeSet = array();
710 262
            }
711 2
712
            foreach ($actualData as $propName => $actualValue) {
713 262
                // skip not saved fields
714
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
715
                    continue;
716 262
                }
717
718 262
                $orgValue = $originalData[$propName] ?? null;
719
720
                // skip if value has not changed
721
                if ($orgValue === $actualValue) {
722 262
                    if (!$actualValue instanceof PersistentCollectionInterface) {
723
                        continue;
724
                    }
725 262
726 261
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
727 261
                        // consider dirty collections as changed as well
728
                        continue;
729
                    }
730 181
                }
731
732 157
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
733
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
734
                    if ($orgValue !== null) {
735
                        $this->scheduleOrphanRemoval($orgValue);
736
                    }
737 224
                    $changeSet[$propName] = array($orgValue, $actualValue);
738 13
                    continue;
739 8
                }
740
741 13
                // if owning side of reference-one relationship
742 13
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
743
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
744
                        $this->scheduleOrphanRemoval($orgValue);
745
                    }
746 218
747 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
748 1
                    continue;
749
                }
750
751 13
                if ($isChangeTrackingNotify) {
752 13
                    continue;
753
                }
754
755 211
                // ignore inverse side of reference relationship
756 3 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...
757
                    continue;
758
                }
759
760 209
                // Persistent collection was exchanged with the "originally"
761 6
                // created one. This can only mean it was cloned and replaced
762
                // on another document.
763
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
764
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
765
                }
766
767 207
                // if embed-many or reference-many relationship
768 6
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
769
                    $changeSet[$propName] = array($orgValue, $actualValue);
770
                    /* If original collection was exchanged with a non-empty value
771
                     * and $set will be issued, there is no need to $unset it first
772 207
                     */
773 100
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
774
                        continue;
775
                    }
776
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
777 100
                        $this->scheduleCollectionDeletion($orgValue);
778 19
                    }
779
                    continue;
780 87
                }
781 15
782
                // skip equivalent date values
783 87
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
784
                    $dateType = Type::getType('date');
785
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
786
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
787 136
788 37
                    if ($dbOrgValue instanceof \MongoDB\BSON\UTCDateTime && $dbActualValue instanceof \MongoDB\BSON\UTCDateTime && $dbOrgValue == $dbActualValue) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
789 37
                        continue;
790 37
                    }
791
                }
792 37
793 30
                // regular field
794
                $changeSet[$propName] = array($orgValue, $actualValue);
795
            }
796
            if ($changeSet) {
797
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
798 119
                    ? $changeSet + $this->documentChangeSets[$oid]
799
                    : $changeSet;
800 262
801 213
                $this->originalDocumentData[$oid] = $actualData;
802 16
                $this->scheduleForUpdate($document);
803 211
            }
804
        }
805 213
806 213
        // Look for changes in associations of the document
807
        $associationMappings = array_filter(
808
            $class->associationMappings,
809
            function ($assoc) { return empty($assoc['notSaved']); }
810
        );
811 576
812 576
        foreach ($associationMappings as $mapping) {
813
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
814
815
            if ($value === null) {
816 576
                continue;
817 437
            }
818
819 437
            $this->computeAssociationChanges($document, $mapping, $value);
820 300
821
            if (isset($mapping['reference'])) {
822
                continue;
823 424
            }
824
825 423
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
826 320
827
            foreach ($values as $obj) {
828
                $oid2 = spl_object_hash($obj);
829 322
830
                if (isset($this->documentChangeSets[$oid2])) {
831 322
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
832 158
                        // instance of $value is the same as it was previously otherwise there would be
833
                        // change set already in place
834 158
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
835 156
                    }
836
837
                    if ( ! $isNewDocument) {
838 34
                        $this->scheduleForUpdate($document);
839
                    }
840
841 156
                    break;
842 65
                }
843
            }
844
        }
845 322
    }
846
847
    /**
848
     * Computes all the changes that have been done to documents and collections
849 575
     * since the last commit and stores these changes in the _documentChangeSet map
850
     * temporarily for access by the persisters, until the UoW commit is finished.
851
     */
852
    public function computeChangeSets()
853
    {
854
        $this->computeScheduleInsertsChangeSets();
855
        $this->computeScheduleUpsertsChangeSets();
856 575
857
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
858 575
        foreach ($this->identityMap as $className => $documents) {
859 574
            $class = $this->dm->getClassMetadata($className);
860
            if ($class->isEmbeddedDocument) {
861
                /* we do not want to compute changes to embedded documents up front
862 574
                 * in case embedded document was replaced and its changeset
863 574
                 * would corrupt data. Embedded documents' change set will
864 574
                 * be calculated by reachability from owning document.
865
                 */
866
                continue;
867
            }
868
869
            // If change tracking is explicit or happens through notification, then only compute
870 149
            // changes on document of that type that are explicitly marked for synchronization.
871
            switch (true) {
872
                case ($class->isChangeTrackingDeferredImplicit()):
873
                    $documentsToProcess = $documents;
874
                    break;
875
876 574
                case (isset($this->scheduledForDirtyCheck[$className])):
877 573
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
878 573
                    break;
879
880 4
                default:
881 3
                    $documentsToProcess = array();
882 3
883
            }
884
885 4
            foreach ($documentsToProcess as $document) {
886
                // Ignore uninitialized proxy objects
887
                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...
888
                    continue;
889 574
                }
890
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
891 570
                $oid = spl_object_hash($document);
892 9 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...
893
                    && ! isset($this->documentUpserts[$oid])
894
                    && ! isset($this->documentDeletions[$oid])
895 570
                    && isset($this->documentStates[$oid])
896 570
                ) {
897 570
                    $this->computeChangeSet($class, $document);
898 570
                }
899 570
            }
900
        }
901 574
    }
902
903
    /**
904
     * Computes the changes of an association.
905 574
     *
906
     * @param object $parentDocument
907
     * @param array $assoc
908
     * @param mixed $value The value of the association.
909
     * @throws \InvalidArgumentException
910
     */
911
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
912
    {
913
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
914
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
915 424
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
916
917 424
        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...
918 424
            return;
919 424
        }
920
921 424
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
922 7
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
923
                $this->scheduleCollectionUpdate($value);
924
            }
925 423
            $topmostOwner = $this->getOwningDocument($value->getOwner());
926 227
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
927 223
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
928
                $value->initialize();
929 227
                foreach ($value->getDeletedDocuments() as $orphan) {
930 227
                    $this->scheduleOrphanRemoval($orphan);
931 227
                }
932 122
            }
933 122
        }
934 20
935
        // Look through the documents, and in any of their associations,
936
        // for transient (new) documents, recursively. ("Persistence by reachability")
937
        // Unwrap. Uninitialized collections will simply be empty.
938
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
939
940
        $count = 0;
941
        foreach ($unwrappedValue as $key => $entry) {
942 423
            if ( ! is_object($entry)) {
943
                throw new \InvalidArgumentException(
944 423
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
945 423
                );
946 336
            }
947 1
948 1
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
949
950
            $state = $this->getDocumentState($entry, self::STATE_NEW);
951
952 335
            // Handle "set" strategy for multi-level hierarchy
953
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
954 335
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
955
956
            $count++;
957 335
958 335
            switch ($state) {
959
                case self::STATE_NEW:
960 335
                    if ( ! $assoc['isCascadePersist']) {
961
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
962
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
963 335
                            . ' Explicitly persist the new document or configure cascading persist operations'
964 53
                            . ' on the relationship.');
965
                    }
966
967
                    $this->persistNew($targetClass, $entry);
968
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
969
                    $this->computeChangeSet($targetClass, $entry);
970
                    break;
971 53
972 53
                case self::STATE_MANAGED:
973 53
                    if ($targetClass->isEmbeddedDocument) {
974 53
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
975
                        if ($knownParent && $knownParent !== $parentDocument) {
976 331
                            $entry = clone $entry;
977 331
                            if ($assoc['type'] === ClassMetadata::ONE) {
978 150
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
979 150
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
980 6
                                $poid = spl_object_hash($parentDocument);
981 6
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
982 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
983 3
                                }
984 3
                            } else {
985 3
                                // must use unwrapped value to not trigger orphan removal
986 3
                                $unwrappedValue[$key] = $entry;
987
                            }
988
                            $this->persistNew($targetClass, $entry);
989
                        }
990 4
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
991
                        $this->computeChangeSet($targetClass, $entry);
992 6
                    }
993
                    break;
994 150
995 150
                case self::STATE_REMOVED:
996
                    // Consume the $value as array (it's either an array or an ArrayAccess)
997 331
                    // and remove the element from Collection.
998
                    if ($assoc['type'] === ClassMetadata::MANY) {
999 1
                        unset($value[$key]);
1000
                    }
1001
                    break;
1002 1
1003
                case self::STATE_DETACHED:
1004
                    // Can actually not happen right now as we assume STATE_NEW,
1005 1
                    // so the exception will be raised from the DBAL layer (constraint violation).
1006
                    throw new \InvalidArgumentException('A detached document was found through a '
1007
                        . 'relationship during cascading a persist operation.');
1008
1009
                default:
1010
                    // MANAGED associated documents are already taken into account
1011
                    // during changeset calculation anyway, since they are in the identity map.
1012
1013 335
            }
1014
        }
1015
    }
1016
1017
    /**
1018
     * INTERNAL:
1019 422
     * Computes the changeset of an individual document, independently of the
1020
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1021
     *
1022
     * The passed document must be a managed document. If the document already has a change set
1023
     * because this method is invoked during a commit cycle then the change sets are added.
1024
     * whereby changes detected in this method prevail.
1025
     *
1026
     * @ignore
1027
     * @param ClassMetadata $class The class descriptor of the document.
1028
     * @param object $document The document for which to (re)calculate the change set.
1029
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1030
     */
1031
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1032
    {
1033
        // Ignore uninitialized proxy objects
1034
        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...
1035 17
            return;
1036
        }
1037
1038 17
        $oid = spl_object_hash($document);
1039 1
1040
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1041
            throw new \InvalidArgumentException('Document must be managed.');
1042 16
        }
1043
1044 16
        if ( ! $class->isInheritanceTypeNone()) {
1045
            $class = $this->dm->getClassMetadata(get_class($document));
1046
        }
1047
1048 16
        $this->computeOrRecomputeChangeSet($class, $document, true);
1049 1
    }
1050
1051
    /**
1052 16
     * @param ClassMetadata $class
1053 16
     * @param object $document
1054
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1055
     */
1056
    private function persistNew(ClassMetadata $class, $document)
1057
    {
1058
        $this->lifecycleEventManager->prePersist($class, $document);
1059
        $oid = spl_object_hash($document);
1060 606
        $upsert = false;
1061
        if ($class->identifier) {
1062 606
            $idValue = $class->getIdentifierValue($document);
1063 606
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1064 606
1065 606
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1066 606
                throw new \InvalidArgumentException(sprintf(
1067 606
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1068
                    get_class($document)
1069 606
                ));
1070 3
            }
1071 3
1072 3
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', $idValue)) {
1073
                throw new \InvalidArgumentException(sprintf(
1074
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1075
                    get_class($document)
1076 605
                ));
1077 1
            }
1078 1
1079 1
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1080
                $idValue = $class->idGenerator->generate($this->dm, $document);
1081
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1082
                $class->setIdentifierValue($document, $idValue);
1083 604
            }
1084 525
1085 525
            $this->documentIdentifiers[$oid] = $idValue;
1086 525
        } else {
1087
            // this is for embedded documents without identifiers
1088
            $this->documentIdentifiers[$oid] = $oid;
1089 604
        }
1090
1091
        $this->documentStates[$oid] = self::STATE_MANAGED;
1092 130
1093
        if ($upsert) {
1094
            $this->scheduleForUpsert($class, $document);
1095 604
        } else {
1096
            $this->scheduleForInsert($class, $document);
1097 604
        }
1098 89
    }
1099
1100 533
    /**
1101
     * Executes all document insertions for documents of the specified type.
1102 604
     *
1103
     * @param ClassMetadata $class
1104
     * @param array $documents Array of documents to insert
1105
     * @param array $options Array of options to be used with batchInsert()
1106
     */
1107 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...
1108
    {
1109
        $persister = $this->getDocumentPersister($class->name);
1110
1111 496
        foreach ($documents as $oid => $document) {
1112
            $persister->addInsert($document);
1113 496
            unset($this->documentInsertions[$oid]);
1114
        }
1115 496
1116 496
        $persister->executeInserts($options);
1117 496
1118
        foreach ($documents as $document) {
1119
            $this->lifecycleEventManager->postPersist($class, $document);
1120 496
        }
1121
    }
1122 495
1123 495
    /**
1124
     * Executes all document upserts for documents of the specified type.
1125 495
     *
1126
     * @param ClassMetadata $class
1127
     * @param array $documents Array of documents to upsert
1128
     * @param array $options Array of options to be used with batchInsert()
1129
     */
1130 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...
1131
    {
1132
        $persister = $this->getDocumentPersister($class->name);
1133
1134 86
1135
        foreach ($documents as $oid => $document) {
1136 86
            $persister->addUpsert($document);
1137
            unset($this->documentUpserts[$oid]);
1138
        }
1139 86
1140 86
        $persister->executeUpserts($options);
1141 86
1142
        foreach ($documents as $document) {
1143
            $this->lifecycleEventManager->postPersist($class, $document);
1144 86
        }
1145
    }
1146 86
1147 86
    /**
1148
     * Executes all document updates for documents of the specified type.
1149 86
     *
1150
     * @param Mapping\ClassMetadata $class
1151
     * @param array $documents Array of documents to update
1152
     * @param array $options Array of options to be used with update()
1153
     */
1154
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1155
    {
1156
        if ($class->isReadOnly) {
1157
            return;
1158 208
        }
1159
1160 208
        $className = $class->name;
1161
        $persister = $this->getDocumentPersister($className);
1162
1163
        foreach ($documents as $oid => $document) {
1164 208
            $this->lifecycleEventManager->preUpdate($class, $document);
1165 208
1166
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1167 208
                $persister->update($document, $options);
1168 208
            }
1169
1170 208
            unset($this->documentUpdates[$oid]);
1171 207
1172
            $this->lifecycleEventManager->postUpdate($class, $document);
1173
        }
1174 204
    }
1175
1176 204
    /**
1177
     * Executes all document deletions for documents of the specified type.
1178 204
     *
1179
     * @param ClassMetadata $class
1180
     * @param array $documents Array of documents to delete
1181
     * @param array $options Array of options to be used with remove()
1182
     */
1183
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1184
    {
1185
        $persister = $this->getDocumentPersister($class->name);
1186
1187 65
        foreach ($documents as $oid => $document) {
1188
            if ( ! $class->isEmbeddedDocument) {
1189 65
                $persister->delete($document, $options);
1190
            }
1191 65
            unset(
1192 65
                $this->documentDeletions[$oid],
1193 33
                $this->documentIdentifiers[$oid],
1194
                $this->originalDocumentData[$oid]
1195
            );
1196 63
1197 63
            // Clear snapshot information for any referenced PersistentCollection
1198 63
            // http://www.doctrine-project.org/jira/browse/MODM-95
1199
            foreach ($class->associationMappings as $fieldMapping) {
1200
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1201
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1202
                    if ($value instanceof PersistentCollectionInterface) {
1203 63
                        $value->clearSnapshot();
1204 38
                    }
1205 24
                }
1206 24
            }
1207 38
1208
            // Document with this $oid after deletion treated as NEW, even if the $oid
1209
            // is obtained by a new document because the old one went out of scope.
1210
            $this->documentStates[$oid] = self::STATE_NEW;
1211
1212
            $this->lifecycleEventManager->postRemove($class, $document);
1213
        }
1214 63
    }
1215
1216 63
    /**
1217
     * Schedules a document for insertion into the database.
1218 63
     * If the document already has an identifier, it will be added to the
1219
     * identity map.
1220
     *
1221
     * @param ClassMetadata $class
1222
     * @param object $document The document to schedule for insertion.
1223
     * @throws \InvalidArgumentException
1224
     */
1225
    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...
1226
    {
1227
        $oid = spl_object_hash($document);
1228
1229 536
        if (isset($this->documentUpdates[$oid])) {
1230
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1231 536
        }
1232
        if (isset($this->documentDeletions[$oid])) {
1233 536
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1234
        }
1235
        if (isset($this->documentInsertions[$oid])) {
1236 536
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1237
        }
1238
1239 536
        $this->documentInsertions[$oid] = $document;
1240
1241
        if (isset($this->documentIdentifiers[$oid])) {
1242
            $this->addToIdentityMap($document);
1243 536
        }
1244
    }
1245 536
1246 533
    /**
1247
     * Schedules a document for upsert into the database and adds it to the
1248 536
     * identity map
1249
     *
1250
     * @param ClassMetadata $class
1251
     * @param object $document The document to schedule for upsert.
1252
     * @throws \InvalidArgumentException
1253
     */
1254
    public function scheduleForUpsert(ClassMetadata $class, $document)
1255
    {
1256
        $oid = spl_object_hash($document);
1257
1258 92
        if ($class->isEmbeddedDocument) {
1259
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1260 92
        }
1261
        if (isset($this->documentUpdates[$oid])) {
1262 92
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1263
        }
1264
        if (isset($this->documentDeletions[$oid])) {
1265 92
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1266
        }
1267
        if (isset($this->documentUpserts[$oid])) {
1268 92
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1269
        }
1270
1271 92
        $this->documentUpserts[$oid] = $document;
1272
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1273
        $this->addToIdentityMap($document);
1274
    }
1275 92
1276 92
    /**
1277 92
     * Checks whether a document is scheduled for insertion.
1278 92
     *
1279
     * @param object $document
1280
     * @return boolean
1281
     */
1282
    public function isScheduledForInsert($document)
1283
    {
1284
        return isset($this->documentInsertions[spl_object_hash($document)]);
1285
    }
1286 92
1287
    /**
1288 92
     * Checks whether a document is scheduled for upsert.
1289
     *
1290
     * @param object $document
1291
     * @return boolean
1292
     */
1293
    public function isScheduledForUpsert($document)
1294
    {
1295
        return isset($this->documentUpserts[spl_object_hash($document)]);
1296
    }
1297 5
1298
    /**
1299 5
     * Schedules a document for being updated.
1300
     *
1301
     * @param object $document The document to schedule for being updated.
1302
     * @throws \InvalidArgumentException
1303
     */
1304
    public function scheduleForUpdate($document)
1305
    {
1306
        $oid = spl_object_hash($document);
1307
        if ( ! isset($this->documentIdentifiers[$oid])) {
1308 214
            throw new \InvalidArgumentException('Document has no identity.');
1309
        }
1310 214
1311 214
        if (isset($this->documentDeletions[$oid])) {
1312
            throw new \InvalidArgumentException('Document is removed.');
1313
        }
1314
1315 214
        if ( ! isset($this->documentUpdates[$oid])
1316
            && ! isset($this->documentInsertions[$oid])
1317
            && ! isset($this->documentUpserts[$oid])) {
1318
            $this->documentUpdates[$oid] = $document;
1319 214
        }
1320 214
    }
1321 214
1322 213
    /**
1323
     * Checks whether a document is registered as dirty in the unit of work.
1324 214
     * Note: Is not very useful currently as dirty documents are only registered
1325
     * at commit time.
1326
     *
1327
     * @param object $document
1328
     * @return boolean
1329
     */
1330
    public function isScheduledForUpdate($document)
1331
    {
1332
        return isset($this->documentUpdates[spl_object_hash($document)]);
1333
    }
1334 15
1335
    public function isScheduledForDirtyCheck($document)
1336 15
    {
1337
        $class = $this->dm->getClassMetadata(get_class($document));
1338
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1339 1
    }
1340
1341 1
    /**
1342 1
     * INTERNAL:
1343
     * Schedules a document for deletion.
1344
     *
1345
     * @param object $document
1346
     */
1347
    public function scheduleForDelete($document)
1348
    {
1349
        $oid = spl_object_hash($document);
1350
1351 70
        if (isset($this->documentInsertions[$oid])) {
1352
            if ($this->isInIdentityMap($document)) {
1353 70
                $this->removeFromIdentityMap($document);
1354
            }
1355 70
            unset($this->documentInsertions[$oid]);
1356 1
            return; // document has not been persisted yet, so nothing more to do.
1357 1
        }
1358
1359 1
        if ( ! $this->isInIdentityMap($document)) {
1360 1
            return; // ignore
1361
        }
1362
1363 69
        $this->removeFromIdentityMap($document);
1364 2
        $this->documentStates[$oid] = self::STATE_REMOVED;
1365
1366
        if (isset($this->documentUpdates[$oid])) {
1367 68
            unset($this->documentUpdates[$oid]);
1368 68
        }
1369
        if ( ! isset($this->documentDeletions[$oid])) {
1370 68
            $this->documentDeletions[$oid] = $document;
1371
        }
1372
    }
1373 68
1374 68
    /**
1375
     * Checks whether a document is registered as removed/deleted with the unit
1376 68
     * of work.
1377
     *
1378
     * @param object $document
1379
     * @return boolean
1380
     */
1381
    public function isScheduledForDelete($document)
1382
    {
1383
        return isset($this->documentDeletions[spl_object_hash($document)]);
1384
    }
1385 8
1386
    /**
1387 8
     * Checks whether a document is scheduled for insertion, update or deletion.
1388
     *
1389
     * @param $document
1390
     * @return boolean
1391
     */
1392
    public function isDocumentScheduled($document)
1393
    {
1394
        $oid = spl_object_hash($document);
1395
        return isset($this->documentInsertions[$oid]) ||
1396 226
            isset($this->documentUpserts[$oid]) ||
1397
            isset($this->documentUpdates[$oid]) ||
1398 226
            isset($this->documentDeletions[$oid]);
1399 226
    }
1400 112
1401 103
    /**
1402 226
     * INTERNAL:
1403
     * Registers a document in the identity map.
1404
     *
1405
     * Note that documents in a hierarchy are registered with the class name of
1406
     * the root document. Identifiers are serialized before being used as array
1407
     * keys to allow differentiation of equal, but not identical, values.
1408
     *
1409
     * @ignore
1410
     * @param object $document  The document to register.
1411
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1412
     *                  the document in question is already managed.
1413
     */
1414
    public function addToIdentityMap($document)
1415
    {
1416
        $class = $this->dm->getClassMetadata(get_class($document));
1417
        $id = $this->getIdForIdentityMap($document);
1418 636
1419
        if (isset($this->identityMap[$class->name][$id])) {
1420 636
            return false;
1421 636
        }
1422
1423 636
        $this->identityMap[$class->name][$id] = $document;
1424 57
1425
        if ($document instanceof NotifyPropertyChanged &&
1426
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1427 636
            $document->addPropertyChangedListener($this);
1428
        }
1429 636
1430 636
        return true;
1431 3
    }
1432
1433
    /**
1434 636
     * Gets the state of a document with regard to the current unit of work.
1435
     *
1436
     * @param object   $document
1437
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1438
     *                         This parameter can be set to improve performance of document state detection
1439
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1440
     *                         is either known or does not matter for the caller of the method.
1441
     * @return int The document state.
1442
     */
1443
    public function getDocumentState($document, $assume = null)
1444
    {
1445
        $oid = spl_object_hash($document);
1446
1447 609
        if (isset($this->documentStates[$oid])) {
1448
            return $this->documentStates[$oid];
1449 609
        }
1450
1451 609
        $class = $this->dm->getClassMetadata(get_class($document));
1452 376
1453
        if ($class->isEmbeddedDocument) {
1454
            return self::STATE_NEW;
1455 609
        }
1456
1457 609
        if ($assume !== null) {
1458 163
            return $assume;
1459
        }
1460
1461 606
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1462 603
         * known. Note that you cannot remember the NEW or DETACHED state in
1463
         * _documentStates since the UoW does not hold references to such
1464
         * objects and the object hash can be reused. More generally, because
1465
         * the state may "change" between NEW/DETACHED without the UoW being
1466
         * aware of it.
1467
         */
1468
        $id = $class->getIdentifierObject($document);
1469
1470
        if ($id === null) {
1471
            return self::STATE_NEW;
1472 4
        }
1473
1474 4
        // Check for a version field, if available, to avoid a DB lookup.
1475 3
        if ($class->isVersioned) {
1476
            return $class->getFieldValue($document, $class->versionField)
1477
                ? self::STATE_DETACHED
1478
                : self::STATE_NEW;
1479 2
        }
1480
1481
        // Last try before DB lookup: check the identity map.
1482
        if ($this->tryGetById($id, $class)) {
1483
            return self::STATE_DETACHED;
1484
        }
1485
1486 2
        // DB lookup
1487 1
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1488
            return self::STATE_DETACHED;
1489
        }
1490
1491 2
        return self::STATE_NEW;
1492 1
    }
1493
1494
    /**
1495 1
     * INTERNAL:
1496
     * Removes a document from the identity map. This effectively detaches the
1497
     * document from the persistence management of Doctrine.
1498
     *
1499
     * @ignore
1500
     * @param object $document
1501
     * @throws \InvalidArgumentException
1502
     * @return boolean
1503
     */
1504
    public function removeFromIdentityMap($document)
1505
    {
1506
        $oid = spl_object_hash($document);
1507
1508 82
        // Check if id is registered first
1509
        if ( ! isset($this->documentIdentifiers[$oid])) {
1510 82
            return false;
1511
        }
1512
1513 82
        $class = $this->dm->getClassMetadata(get_class($document));
1514
        $id = $this->getIdForIdentityMap($document);
1515
1516
        if (isset($this->identityMap[$class->name][$id])) {
1517 82
            unset($this->identityMap[$class->name][$id]);
1518 82
            $this->documentStates[$oid] = self::STATE_DETACHED;
1519
            return true;
1520 82
        }
1521 82
1522 82
        return false;
1523 82
    }
1524
1525
    /**
1526
     * INTERNAL:
1527
     * Gets a document in the identity map by its identifier hash.
1528
     *
1529
     * @ignore
1530
     * @param mixed         $id    Document identifier
1531
     * @param ClassMetadata $class Document class
1532
     * @return object
1533
     * @throws InvalidArgumentException if the class does not have an identifier
1534
     */
1535 View Code Duplication
    public function getById($id, ClassMetadata $class)
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...
1536
    {
1537
        if ( ! $class->identifier) {
1538
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1539 38
        }
1540
1541 38
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1542
1543
        return $this->identityMap[$class->name][$serializedId];
1544
    }
1545 38
1546
    /**
1547 38
     * INTERNAL:
1548
     * Tries to get a document by its identifier hash. If no document is found
1549
     * for the given hash, FALSE is returned.
1550
     *
1551
     * @ignore
1552
     * @param mixed         $id    Document identifier
1553
     * @param ClassMetadata $class Document class
1554
     * @return mixed The found document or FALSE.
1555
     * @throws InvalidArgumentException if the class does not have an identifier
1556
     */
1557 View Code Duplication
    public function tryGetById($id, ClassMetadata $class)
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...
1558
    {
1559
        if ( ! $class->identifier) {
1560
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1561 298
        }
1562
1563 298
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1564
1565
        return $this->identityMap[$class->name][$serializedId] ?? false;
1566
    }
1567 298
1568
    /**
1569 298
     * Schedules a document for dirty-checking at commit-time.
1570 298
     *
1571
     * @param object $document The document to schedule for dirty-checking.
1572
     * @todo Rename: scheduleForSynchronization
1573
     */
1574
    public function scheduleForDirtyCheck($document)
1575
    {
1576
        $class = $this->dm->getClassMetadata(get_class($document));
1577
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1578
    }
1579 3
1580
    /**
1581 3
     * Checks whether a document is registered in the identity map.
1582 3
     *
1583 3
     * @param object $document
1584
     * @return boolean
1585
     */
1586
    public function isInIdentityMap($document)
1587
    {
1588
        $oid = spl_object_hash($document);
1589
1590
        if ( ! isset($this->documentIdentifiers[$oid])) {
1591 81
            return false;
1592
        }
1593 81
1594
        $class = $this->dm->getClassMetadata(get_class($document));
1595 81
        $id = $this->getIdForIdentityMap($document);
1596 6
1597
        return isset($this->identityMap[$class->name][$id]);
1598
    }
1599 79
1600 79
    /**
1601
     * @param object $document
1602 79
     * @return string
1603
     */
1604
    private function getIdForIdentityMap($document)
1605
    {
1606
        $class = $this->dm->getClassMetadata(get_class($document));
1607
1608
        if ( ! $class->identifier) {
1609 636
            $id = spl_object_hash($document);
1610
        } else {
1611 636
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1612
            $id = serialize($class->getDatabaseIdentifierValue($id));
1613 636
        }
1614 133
1615
        return $id;
1616 635
    }
1617 635
1618
    /**
1619
     * INTERNAL:
1620 636
     * Checks whether an identifier exists in the identity map.
1621
     *
1622
     * @ignore
1623
     * @param string $id
1624
     * @param string $rootClassName
1625
     * @return boolean
1626
     */
1627
    public function containsId($id, $rootClassName)
1628
    {
1629
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1630
    }
1631
1632
    /**
1633
     * Persists a document as part of the current unit of work.
1634
     *
1635
     * @param object $document The document to persist.
1636
     * @throws MongoDBException If trying to persist MappedSuperclass.
1637
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1638
     */
1639
    public function persist($document)
1640
    {
1641
        $class = $this->dm->getClassMetadata(get_class($document));
1642
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1643
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1644 606
        }
1645
        $visited = array();
1646 606
        $this->doPersist($document, $visited);
1647 606
    }
1648 1
1649
    /**
1650 605
     * Saves a document as part of the current unit of work.
1651 605
     * This method is internally called during save() cascades as it tracks
1652 601
     * the already visited documents to prevent infinite recursions.
1653
     *
1654
     * NOTE: This method always considers documents that are not yet known to
1655
     * this UnitOfWork as NEW.
1656
     *
1657
     * @param object $document The document to persist.
1658
     * @param array $visited The already visited documents.
1659
     * @throws \InvalidArgumentException
1660
     * @throws MongoDBException
1661
     */
1662
    private function doPersist($document, array &$visited)
1663
    {
1664
        $oid = spl_object_hash($document);
1665
        if (isset($visited[$oid])) {
1666
            return; // Prevent infinite recursion
1667 605
        }
1668
1669 605
        $visited[$oid] = $document; // Mark visited
1670 605
1671 25
        $class = $this->dm->getClassMetadata(get_class($document));
1672
1673
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1674 605
        switch ($documentState) {
1675
            case self::STATE_MANAGED:
1676 605
                // Nothing to do, except if policy is "deferred explicit"
1677
                if ($class->isChangeTrackingDeferredExplicit()) {
1678 605
                    $this->scheduleForDirtyCheck($document);
1679
                }
1680 605
                break;
1681
            case self::STATE_NEW:
1682 58
                $this->persistNew($class, $document);
1683
                break;
1684
1685 58
            case self::STATE_REMOVED:
1686 605
                // Document becomes managed again
1687 605
                unset($this->documentDeletions[$oid]);
1688 603
1689
                $this->documentStates[$oid] = self::STATE_MANAGED;
1690 2
                break;
1691
1692 2
            case self::STATE_DETACHED:
1693
                throw new \InvalidArgumentException(
1694 2
                    'Behavior of persist() for a detached document is not yet defined.');
1695 2
1696
            default:
1697
                throw MongoDBException::invalidDocumentState($documentState);
1698
        }
1699
1700
        $this->cascadePersist($document, $visited);
1701
    }
1702
1703
    /**
1704
     * Deletes a document as part of the current unit of work.
1705 603
     *
1706 601
     * @param object $document The document to remove.
1707
     */
1708
    public function remove($document)
1709
    {
1710
        $visited = array();
1711
        $this->doRemove($document, $visited);
1712
    }
1713 69
1714
    /**
1715 69
     * Deletes a document as part of the current unit of work.
1716 69
     *
1717 69
     * This method is internally called during delete() cascades as it tracks
1718
     * the already visited documents to prevent infinite recursions.
1719
     *
1720
     * @param object $document The document to delete.
1721
     * @param array $visited The map of the already visited documents.
1722
     * @throws MongoDBException
1723
     */
1724
    private function doRemove($document, array &$visited)
1725
    {
1726
        $oid = spl_object_hash($document);
1727
        if (isset($visited[$oid])) {
1728
            return; // Prevent infinite recursion
1729 69
        }
1730
1731 69
        $visited[$oid] = $document; // mark visited
1732 69
1733 1
        /* Cascade first, because scheduleForDelete() removes the entity from
1734
         * the identity map, which can cause problems when a lazy Proxy has to
1735
         * be initialized for the cascade operation.
1736 69
         */
1737
        $this->cascadeRemove($document, $visited);
1738
1739
        $class = $this->dm->getClassMetadata(get_class($document));
1740
        $documentState = $this->getDocumentState($document);
1741
        switch ($documentState) {
1742 69
            case self::STATE_NEW:
1743
            case self::STATE_REMOVED:
1744 69
                // nothing to do
1745 69
                break;
1746
            case self::STATE_MANAGED:
1747 69
                $this->lifecycleEventManager->preRemove($class, $document);
1748 69
                $this->scheduleForDelete($document);
1749
                break;
1750
            case self::STATE_DETACHED:
1751 69
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1752 69
            default:
1753 69
                throw MongoDBException::invalidDocumentState($documentState);
1754 69
        }
1755
    }
1756
1757
    /**
1758
     * Merges the state of the given detached document into this UnitOfWork.
1759
     *
1760 69
     * @param object $document
1761
     * @return object The managed copy of the document.
1762
     */
1763
    public function merge($document)
1764
    {
1765
        $visited = array();
1766
1767
        return $this->doMerge($document, $visited);
1768 12
    }
1769
1770 12
    /**
1771
     * Executes a merge operation on a document.
1772 12
     *
1773
     * @param object      $document
1774
     * @param array       $visited
1775
     * @param object|null $prevManagedCopy
1776
     * @param array|null  $assoc
1777
     *
1778
     * @return object The managed copy of the document.
1779
     *
1780
     * @throws InvalidArgumentException If the entity instance is NEW.
1781
     * @throws LockException If the document uses optimistic locking through a
1782
     *                       version attribute and the version check against the
1783
     *                       managed copy fails.
1784
     */
1785
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1786
    {
1787
        $oid = spl_object_hash($document);
1788
1789
        if (isset($visited[$oid])) {
1790 12
            return $visited[$oid]; // Prevent infinite recursion
1791
        }
1792 12
1793
        $visited[$oid] = $document; // mark visited
1794 12
1795 1
        $class = $this->dm->getClassMetadata(get_class($document));
1796
1797
        /* First we assume DETACHED, although it can still be NEW but we can
1798 12
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1799
         * an identity, we need to fetch it from the DB anyway in order to
1800 12
         * merge. MANAGED documents are ignored by the merge operation.
1801
         */
1802
        $managedCopy = $document;
1803
1804
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1805
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1806
                $document->__load();
1807 12
            }
1808
1809 12
            $identifier = $class->getIdentifier();
1810 12
            // We always have one element in the identifier array but it might be null
1811
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1812
            $managedCopy = null;
1813
1814 12
            // Try to fetch document from the database
1815
            if (! $class->isEmbeddedDocument && $id !== null) {
1816 12
                $managedCopy = $this->dm->find($class->name, $id);
1817 12
1818
                // Managed copy may be removed in which case we can't merge
1819
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1820 12
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1821 12
                }
1822
1823
                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...
1824 12
                    $managedCopy->__load();
1825
                }
1826
            }
1827
1828 12
            if ($managedCopy === null) {
1829
                // Create a new managed instance
1830
                $managedCopy = $class->newInstance();
1831
                if ($id !== null) {
1832
                    $class->setIdentifierValue($managedCopy, $id);
1833 12
                }
1834
                $this->persistNew($class, $managedCopy);
1835 4
            }
1836 4
1837 3
            if ($class->isVersioned) {
1838
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1839 4
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1840
1841
                // Throw exception if versions don't match
1842 12
                if ($managedCopyVersion != $documentVersion) {
1843
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1844
                }
1845
            }
1846
1847
            // Merge state of $document into existing (managed) document
1848
            foreach ($class->reflClass->getProperties() as $prop) {
1849
                $name = $prop->name;
1850
                $prop->setAccessible(true);
1851
                if ( ! isset($class->associationMappings[$name])) {
1852
                    if ( ! $class->isIdentifier($name)) {
1853 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1854 12
                    }
1855 12
                } else {
1856 12
                    $assoc2 = $class->associationMappings[$name];
1857 12
1858 12
                    if ($assoc2['type'] === 'one') {
1859
                        $other = $prop->getValue($document);
1860
1861 12
                        if ($other === null) {
1862
                            $prop->setValue($managedCopy, null);
1863 12
                        } 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...
1864 6
                            // Do not merge fields marked lazy that have not been fetched
1865
                            continue;
1866 6
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1867 2
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1868 5
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1869
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1870 1
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1871 4
                                $relatedId = $targetClass->getIdentifierObject($other);
1872
1873
                                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...
1874
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1875
                                } else {
1876
                                    $other = $this
1877
                                        ->dm
1878
                                        ->getProxyFactory()
1879
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1880
                                    $this->registerManaged($other, $relatedId, array());
1881
                                }
1882
                            }
1883
1884
                            $prop->setValue($managedCopy, $other);
1885
                        }
1886
                    } else {
1887
                        $mergeCol = $prop->getValue($document);
1888
1889 5
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1890
                            /* Do not merge fields marked lazy that have not
1891
                             * been fetched. Keep the lazy persistent collection
1892 10
                             * of the managed copy.
1893
                             */
1894 10
                            continue;
1895
                        }
1896
1897
                        $managedCol = $prop->getValue($managedCopy);
1898
1899 3
                        if ( ! $managedCol) {
1900
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1901
                            $managedCol->setOwner($managedCopy, $assoc2);
1902 10
                            $prop->setValue($managedCopy, $managedCol);
1903
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1904 10
                        }
1905 1
1906 1
                        /* Note: do not process association's target documents.
1907 1
                         * They will be handled during the cascade. Initialize
1908 1
                         * and, if necessary, clear $managedCol for now.
1909
                         */
1910
                        if ($assoc2['isCascadeMerge']) {
1911
                            $managedCol->initialize();
1912
1913
                            // If $managedCol differs from the merged collection, clear and set dirty
1914
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1915 10
                                $managedCol->unwrap()->clear();
1916 10
                                $managedCol->setDirty(true);
1917
1918
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1919 10
                                    $this->scheduleForDirtyCheck($managedCopy);
1920 3
                                }
1921 3
                            }
1922
                        }
1923 3
                    }
1924
                }
1925
1926
                if ($class->isChangeTrackingNotify()) {
1927
                    // Just treat all properties as changed, there is no other choice.
1928
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1929
                }
1930
            }
1931 12
1932
            if ($class->isChangeTrackingDeferredExplicit()) {
1933 12
                $this->scheduleForDirtyCheck($document);
1934
            }
1935
        }
1936
1937 12
        if ($prevManagedCopy !== null) {
1938
            $assocField = $assoc['fieldName'];
1939
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1940
1941
            if ($assoc['type'] === 'one') {
1942 12
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1943 5
            } else {
1944 5
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1945
1946 5
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1947 3
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1948
                }
1949 4
            }
1950
        }
1951 4
1952 1
        // Mark the managed copy visited as well
1953
        $visited[spl_object_hash($managedCopy)] = true;
1954
1955
        $this->cascadeMerge($document, $managedCopy, $visited);
1956
1957
        return $managedCopy;
1958 12
    }
1959
1960 12
    /**
1961
     * Detaches a document from the persistence management. It's persistence will
1962 12
     * no longer be managed by Doctrine.
1963
     *
1964
     * @param object $document The document to detach.
1965
     */
1966
    public function detach($document)
1967
    {
1968
        $visited = array();
1969
        $this->doDetach($document, $visited);
1970
    }
1971 11
1972
    /**
1973 11
     * Executes a detach operation on the given document.
1974 11
     *
1975 11
     * @param object $document
1976
     * @param array $visited
1977
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1978
     */
1979
    private function doDetach($document, array &$visited)
1980
    {
1981
        $oid = spl_object_hash($document);
1982
        if (isset($visited[$oid])) {
1983
            return; // Prevent infinite recursion
1984 16
        }
1985
1986 16
        $visited[$oid] = $document; // mark visited
1987 16
1988 3
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1989
            case self::STATE_MANAGED:
1990
                $this->removeFromIdentityMap($document);
1991 16
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1992
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1993 16
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1994 16
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1995 16
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
1996 16
                break;
1997 16
            case self::STATE_NEW:
1998 16
            case self::STATE_DETACHED:
1999 16
                return;
2000 16
        }
2001 16
2002 3
        $this->cascadeDetach($document, $visited);
2003 3
    }
2004 3
2005
    /**
2006
     * Refreshes the state of the given document from the database, overwriting
2007 16
     * any local, unpersisted changes.
2008 16
     *
2009
     * @param object $document The document to refresh.
2010
     * @throws \InvalidArgumentException If the document is not MANAGED.
2011
     */
2012
    public function refresh($document)
2013
    {
2014
        $visited = array();
2015
        $this->doRefresh($document, $visited);
2016
    }
2017 21
2018
    /**
2019 21
     * Executes a refresh operation on a document.
2020 21
     *
2021 20
     * @param object $document The document to refresh.
2022
     * @param array $visited The already visited documents during cascades.
2023
     * @throws \InvalidArgumentException If the document is not MANAGED.
2024
     */
2025
    private function doRefresh($document, array &$visited)
2026
    {
2027
        $oid = spl_object_hash($document);
2028
        if (isset($visited[$oid])) {
2029
            return; // Prevent infinite recursion
2030 21
        }
2031
2032 21
        $visited[$oid] = $document; // mark visited
2033 21
2034
        $class = $this->dm->getClassMetadata(get_class($document));
2035
2036
        if ( ! $class->isEmbeddedDocument) {
2037 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2038
                $this->getDocumentPersister($class->name)->refresh($document);
2039 21
            } else {
2040
                throw new \InvalidArgumentException('Document is not MANAGED.');
2041 21
            }
2042 21
        }
2043 20
2044
        $this->cascadeRefresh($document, $visited);
2045 1
    }
2046
2047
    /**
2048
     * Cascades a refresh operation to associated documents.
2049 20
     *
2050 20
     * @param object $document
2051
     * @param array $visited
2052
     */
2053
    private function cascadeRefresh($document, array &$visited)
2054
    {
2055
        $class = $this->dm->getClassMetadata(get_class($document));
2056
2057
        $associationMappings = array_filter(
2058 20
            $class->associationMappings,
2059
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2060 20
        );
2061
2062 20
        foreach ($associationMappings as $mapping) {
2063 20
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2064
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2065
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2066
                    // Unwrap so that foreach() does not initialize
2067 20
                    $relatedDocuments = $relatedDocuments->unwrap();
2068 15
                }
2069 15
                foreach ($relatedDocuments as $relatedDocument) {
2070 15
                    $this->doRefresh($relatedDocument, $visited);
2071
                }
2072 15
            } elseif ($relatedDocuments !== null) {
2073
                $this->doRefresh($relatedDocuments, $visited);
2074 15
            }
2075 15
        }
2076
    }
2077 10
2078 15
    /**
2079
     * Cascades a detach operation to associated documents.
2080
     *
2081 20
     * @param object $document
2082
     * @param array $visited
2083
     */
2084 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...
2085
    {
2086
        $class = $this->dm->getClassMetadata(get_class($document));
2087
        foreach ($class->fieldMappings as $mapping) {
2088
            if ( ! $mapping['isCascadeDetach']) {
2089 16
                continue;
2090
            }
2091 16
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2092 16
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2093 16
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2094 16
                    // Unwrap so that foreach() does not initialize
2095
                    $relatedDocuments = $relatedDocuments->unwrap();
2096 10
                }
2097 10
                foreach ($relatedDocuments as $relatedDocument) {
2098 10
                    $this->doDetach($relatedDocument, $visited);
2099
                }
2100 7
            } elseif ($relatedDocuments !== null) {
2101
                $this->doDetach($relatedDocuments, $visited);
2102 10
            }
2103 10
        }
2104
    }
2105 10
    /**
2106 10
     * Cascades a merge operation to associated documents.
2107
     *
2108
     * @param object $document
2109 16
     * @param object $managedCopy
2110
     * @param array $visited
2111
     */
2112
    private function cascadeMerge($document, $managedCopy, array &$visited)
2113
    {
2114
        $class = $this->dm->getClassMetadata(get_class($document));
2115
2116
        $associationMappings = array_filter(
2117 12
            $class->associationMappings,
2118
            function ($assoc) { return $assoc['isCascadeMerge']; }
2119 12
        );
2120
2121 12
        foreach ($associationMappings as $assoc) {
2122 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2123
2124
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2125
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2126 12
                    // Collections are the same, so there is nothing to do
2127 11
                    continue;
2128
                }
2129 11
2130 8
                foreach ($relatedDocuments as $relatedDocument) {
2131
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2132 1
                }
2133
            } elseif ($relatedDocuments !== null) {
2134
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2135 8
            }
2136 8
        }
2137
    }
2138 6
2139 11
    /**
2140
     * Cascades the save operation to associated documents.
2141
     *
2142 12
     * @param object $document
2143
     * @param array $visited
2144
     */
2145
    private function cascadePersist($document, array &$visited)
2146
    {
2147
        $class = $this->dm->getClassMetadata(get_class($document));
2148
2149
        $associationMappings = array_filter(
2150 603
            $class->associationMappings,
2151
            function ($assoc) { return $assoc['isCascadePersist']; }
2152 603
        );
2153
2154 603
        foreach ($associationMappings as $fieldName => $mapping) {
2155 603
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2156
2157
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2158
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2159 603
                    if ($relatedDocuments->getOwner() !== $document) {
2160 417
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2161
                    }
2162 417
                    // Unwrap so that foreach() does not initialize
2163 344
                    $relatedDocuments = $relatedDocuments->unwrap();
2164 17
                }
2165 2
2166
                $count = 0;
2167
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2168 17
                    if ( ! empty($mapping['embedded'])) {
2169
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2170
                        if ($knownParent && $knownParent !== $document) {
2171 344
                            $relatedDocument = clone $relatedDocument;
2172 344
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2173 174
                        }
2174 103
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2175 103
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2176 1
                    }
2177 1
                    $this->doPersist($relatedDocument, $visited);
2178
                }
2179 103
            } elseif ($relatedDocuments !== null) {
2180 103
                if ( ! empty($mapping['embedded'])) {
2181
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2182 344
                    if ($knownParent && $knownParent !== $document) {
2183
                        $relatedDocuments = clone $relatedDocuments;
2184 332
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2185 130
                    }
2186 67
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2187 67
                }
2188 3
                $this->doPersist($relatedDocuments, $visited);
2189 3
            }
2190
        }
2191 67
    }
2192
2193 417
    /**
2194
     * Cascades the delete operation to associated documents.
2195
     *
2196 601
     * @param object $document
2197
     * @param array $visited
2198
     */
2199 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...
2200
    {
2201
        $class = $this->dm->getClassMetadata(get_class($document));
2202
        foreach ($class->fieldMappings as $mapping) {
2203
            if ( ! $mapping['isCascadeRemove']) {
2204 69
                continue;
2205
            }
2206 69
            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...
2207 69
                $document->__load();
2208 69
            }
2209 68
2210
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2211 33
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2212 2
                // If its a PersistentCollection initialization is intended! No unwrap!
2213
                foreach ($relatedDocuments as $relatedDocument) {
2214
                    $this->doRemove($relatedDocument, $visited);
2215 33
                }
2216 33
            } elseif ($relatedDocuments !== null) {
2217
                $this->doRemove($relatedDocuments, $visited);
2218 22
            }
2219 22
        }
2220
    }
2221 22
2222 33
    /**
2223
     * Acquire a lock on the given document.
2224
     *
2225 69
     * @param object $document
2226
     * @param int $lockMode
2227
     * @param int $lockVersion
2228
     * @throws LockException
2229
     * @throws \InvalidArgumentException
2230
     */
2231
    public function lock($document, $lockMode, $lockVersion = null)
2232
    {
2233
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2234
            throw new \InvalidArgumentException('Document is not MANAGED.');
2235
        }
2236 8
2237
        $documentName = get_class($document);
2238 8
        $class = $this->dm->getClassMetadata($documentName);
2239 1
2240
        if ($lockMode == LockMode::OPTIMISTIC) {
2241
            if ( ! $class->isVersioned) {
2242 7
                throw LockException::notVersioned($documentName);
2243 7
            }
2244
2245 7
            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...
2246 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2247 1
                if ($documentVersion != $lockVersion) {
2248
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2249
                }
2250 1
            }
2251 1
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2252 1
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2253 1
        }
2254
    }
2255
2256 5
    /**
2257 5
     * Releases a lock on the given document.
2258
     *
2259 5
     * @param object $document
2260
     * @throws \InvalidArgumentException
2261
     */
2262
    public function unlock($document)
2263
    {
2264
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2265
            throw new \InvalidArgumentException("Document is not MANAGED.");
2266
        }
2267 1
        $documentName = get_class($document);
2268
        $this->getDocumentPersister($documentName)->unlock($document);
2269 1
    }
2270
2271
    /**
2272 1
     * Clears the UnitOfWork.
2273 1
     *
2274 1
     * @param string|null $documentName if given, only documents of this type will get detached.
2275
     */
2276
    public function clear($documentName = null)
2277
    {
2278
        if ($documentName === null) {
2279
            $this->identityMap =
2280
            $this->documentIdentifiers =
2281 377
            $this->originalDocumentData =
2282
            $this->documentChangeSets =
2283 377
            $this->documentStates =
2284 369
            $this->scheduledForDirtyCheck =
2285 369
            $this->documentInsertions =
2286 369
            $this->documentUpserts =
2287 369
            $this->documentUpdates =
2288 369
            $this->documentDeletions =
2289 369
            $this->collectionUpdates =
2290 369
            $this->collectionDeletions =
2291 369
            $this->parentAssociations =
2292 369
            $this->embeddedDocumentsRegistry =
2293 369
            $this->orphanRemovals =
2294 369
            $this->hasScheduledCollections = array();
2295 369
        } else {
2296 369
            $visited = array();
2297 369
            foreach ($this->identityMap as $className => $documents) {
2298 369
                if ($className === $documentName) {
2299 369
                    foreach ($documents as $document) {
2300
                        $this->doDetach($document, $visited);
2301 8
                    }
2302 8
                }
2303 8
            }
2304 5
        }
2305 8
2306 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...
2307
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2308
        }
2309
    }
2310
2311 377
    /**
2312
     * INTERNAL:
2313
     * Schedules an embedded document for removal. The remove() operation will be
2314 377
     * invoked on that document at the beginning of the next commit of this
2315
     * UnitOfWork.
2316
     *
2317
     * @ignore
2318
     * @param object $document
2319
     */
2320
    public function scheduleOrphanRemoval($document)
2321
    {
2322
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2323
    }
2324
2325 47
    /**
2326
     * INTERNAL:
2327 47
     * Unschedules an embedded or referenced object for removal.
2328 47
     *
2329
     * @ignore
2330
     * @param object $document
2331
     */
2332
    public function unscheduleOrphanRemoval($document)
2333
    {
2334
        $oid = spl_object_hash($document);
2335
        if (isset($this->orphanRemovals[$oid])) {
2336
            unset($this->orphanRemovals[$oid]);
2337 100
        }
2338
    }
2339 100
2340 100
    /**
2341 1
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2342
     *  1) sets owner if it was cloned
2343 100
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2344
     *  3) NOP if state is OK
2345
     * Returned collection should be used from now on (only important with 2nd point)
2346
     *
2347
     * @param PersistentCollectionInterface $coll
2348
     * @param object $document
2349
     * @param ClassMetadata $class
2350
     * @param string $propName
2351
     * @return PersistentCollectionInterface
2352
     */
2353
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2354
    {
2355
        $owner = $coll->getOwner();
2356
        if ($owner === null) { // cloned
2357
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2358 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2359
            if ( ! $coll->isInitialized()) {
2360 8
                $coll->initialize(); // we have to do this otherwise the cols share state
2361 8
            }
2362 6
            $newValue = clone $coll;
2363 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2364 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2365 1
            if ($this->isScheduledForUpdate($document)) {
2366
                // @todo following line should be superfluous once collections are stored in change sets
2367 2
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2368 2
            }
2369 2
            return $newValue;
2370 2
        }
2371
        return $coll;
2372
    }
2373
2374 2
    /**
2375
     * INTERNAL:
2376 6
     * Schedules a complete collection for removal when this UnitOfWork commits.
2377
     *
2378
     * @param PersistentCollectionInterface $coll
2379
     */
2380
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2381
    {
2382
        $oid = spl_object_hash($coll);
2383
        unset($this->collectionUpdates[$oid]);
2384
        if ( ! isset($this->collectionDeletions[$oid])) {
2385 35
            $this->collectionDeletions[$oid] = $coll;
2386
            $this->scheduleCollectionOwner($coll);
2387 35
        }
2388 35
    }
2389 35
2390 35
    /**
2391 35
     * Checks whether a PersistentCollection is scheduled for deletion.
2392
     *
2393 35
     * @param PersistentCollectionInterface $coll
2394
     * @return boolean
2395
     */
2396
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2397
    {
2398
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2399
    }
2400
2401 195
    /**
2402
     * INTERNAL:
2403 195
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2404
     *
2405
     * @param PersistentCollectionInterface $coll
2406
     */
2407 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...
2408
    {
2409
        $oid = spl_object_hash($coll);
2410
        if (isset($this->collectionDeletions[$oid])) {
2411
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2412 202
            unset($this->collectionDeletions[$oid]);
2413
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2414 202
        }
2415 202
    }
2416 5
2417 5
    /**
2418 5
     * INTERNAL:
2419
     * Schedules a collection for update when this UnitOfWork commits.
2420 202
     *
2421
     * @param PersistentCollectionInterface $coll
2422
     */
2423
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2424
    {
2425
        $mapping = $coll->getMapping();
2426
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2427
            /* There is no need to $unset collection if it will be $set later
2428 223
             * This is NOP if collection is not scheduled for deletion
2429
             */
2430 223
            $this->unscheduleCollectionDeletion($coll);
2431 223
        }
2432
        $oid = spl_object_hash($coll);
2433
        if ( ! isset($this->collectionUpdates[$oid])) {
2434
            $this->collectionUpdates[$oid] = $coll;
2435 23
            $this->scheduleCollectionOwner($coll);
2436
        }
2437 223
    }
2438 223
2439 223
    /**
2440 223
     * INTERNAL:
2441
     * Unschedules a collection from being updated when this UnitOfWork commits.
2442 223
     *
2443
     * @param PersistentCollectionInterface $coll
2444
     */
2445 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...
2446
    {
2447
        $oid = spl_object_hash($coll);
2448
        if (isset($this->collectionUpdates[$oid])) {
2449
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2450 202
            unset($this->collectionUpdates[$oid]);
2451
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2452 202
        }
2453 202
    }
2454 192
2455 192
    /**
2456 192
     * Checks whether a PersistentCollection is scheduled for update.
2457
     *
2458 202
     * @param PersistentCollectionInterface $coll
2459
     * @return boolean
2460
     */
2461
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2462
    {
2463
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2464
    }
2465
2466 113
    /**
2467
     * INTERNAL:
2468 113
     * Gets PersistentCollections that have been visited during computing change
2469
     * set of $document
2470
     *
2471
     * @param object $document
2472
     * @return PersistentCollectionInterface[]
2473
     */
2474
    public function getVisitedCollections($document)
2475
    {
2476
        $oid = spl_object_hash($document);
2477
2478
        return $this->visitedCollections[$oid] ?? array();
2479 557
    }
2480
2481 557
    /**
2482 557
     * INTERNAL:
2483 226
     * Gets PersistentCollections that are scheduled to update and related to $document
2484 557
     *
2485
     * @param object $document
2486
     * @return array
2487
     */
2488
    public function getScheduledCollections($document)
2489
    {
2490
        $oid = spl_object_hash($document);
2491
2492
        return $this->hasScheduledCollections[$oid] ?? array();
2493
    }
2494 557
2495
    /**
2496 557
     * Checks whether the document is related to a PersistentCollection
2497 557
     * scheduled for update or deletion.
2498 224
     *
2499 557
     * @param object $document
2500
     * @return boolean
2501
     */
2502
    public function hasScheduledCollections($document)
2503
    {
2504
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2505
    }
2506
2507
    /**
2508
     * Marks the PersistentCollection's top-level owner as having a relation to
2509 44
     * a collection scheduled for update or deletion.
2510
     *
2511 44
     * If the owner is not scheduled for any lifecycle action, it will be
2512
     * scheduled for update to ensure that versioning takes place if necessary.
2513
     *
2514
     * If the collection is nested within atomic collection, it is immediately
2515
     * unscheduled and atomic one is scheduled for update instead. This makes
2516
     * calculating update data way easier.
2517
     *
2518
     * @param PersistentCollectionInterface $coll
2519
     */
2520
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2521
    {
2522
        $document = $this->getOwningDocument($coll->getOwner());
2523
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2524
2525
        if ($document !== $coll->getOwner()) {
2526
            $parent = $coll->getOwner();
2527 225
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2528
                list($mapping, $parent, ) = $parentAssoc;
2529 225
            }
2530 225
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2531
                $class = $this->dm->getClassMetadata(get_class($document));
2532 225
                $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...
2533 19
                $this->scheduleCollectionUpdate($atomicCollection);
2534 19
                $this->unscheduleCollectionDeletion($coll);
2535 19
                $this->unscheduleCollectionUpdate($coll);
2536
            }
2537 19
        }
2538 3
2539 3
        if ( ! $this->isDocumentScheduled($document)) {
2540 3
            $this->scheduleForUpdate($document);
2541 3
        }
2542 3
    }
2543
2544
    /**
2545
     * Get the top-most owning document of a given document
2546 225
     *
2547 39
     * If a top-level document is provided, that same document will be returned.
2548
     * For an embedded document, we will walk through parent associations until
2549 225
     * we find a top-level document.
2550
     *
2551
     * @param object $document
2552
     * @throws \UnexpectedValueException when a top-level document could not be found
2553
     * @return object
2554
     */
2555
    public function getOwningDocument($document)
2556
    {
2557
        $class = $this->dm->getClassMetadata(get_class($document));
2558
        while ($class->isEmbeddedDocument) {
2559
            $parentAssociation = $this->getParentAssociation($document);
2560
2561
            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...
2562 227
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2563
            }
2564 227
2565 227
            list(, $document, ) = $parentAssociation;
2566 33
            $class = $this->dm->getClassMetadata(get_class($document));
2567
        }
2568 33
2569
        return $document;
2570
    }
2571
2572 33
    /**
2573 33
     * Gets the class name for an association (embed or reference) with respect
2574
     * to any discriminator value.
2575
     *
2576 227
     * @param array      $mapping Field mapping for the association
2577
     * @param array|null $data    Data for the embedded document or reference
2578
     * @return string Class name.
2579
     */
2580
    public function getClassNameForAssociation(array $mapping, $data)
2581
    {
2582
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2583
2584
        $discriminatorValue = null;
2585
        if (isset($discriminatorField, $data[$discriminatorField])) {
2586
            $discriminatorValue = $data[$discriminatorField];
2587 218
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2588
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2589 218
        }
2590
2591 218
        if ($discriminatorValue !== null) {
2592 218
            return $mapping['discriminatorMap'][$discriminatorValue]
2593 21
                ?? $discriminatorValue;
2594 198
        }
2595
2596
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2597
2598 218 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...
2599 21
            $discriminatorValue = $data[$class->discriminatorField];
2600 21
        } elseif ($class->defaultDiscriminatorValue !== null) {
2601
            $discriminatorValue = $class->defaultDiscriminatorValue;
2602
        }
2603 198
2604
        if ($discriminatorValue !== null) {
2605 198
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2606 15
        }
2607 184
2608 1
        return $mapping['targetDocument'];
2609
    }
2610
2611 198
    /**
2612 16
     * INTERNAL:
2613 14
     * Creates a document. Used for reconstitution of documents during hydration.
2614 16
     *
2615
     * @ignore
2616
     * @param string $className The name of the document class.
2617 183
     * @param array $data The data for the document.
2618
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2619
     * @param object $document The document to be hydrated into in case of creation
2620
     * @return object The document instance.
2621
     * @internal Highly performance-sensitive method.
2622
     */
2623
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2624
    {
2625
        $class = $this->dm->getClassMetadata($className);
2626
2627
        // @TODO figure out how to remove this
2628
        $discriminatorValue = null;
2629 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...
2630
            $discriminatorValue = $data[$class->discriminatorField];
2631
        } elseif (isset($class->defaultDiscriminatorValue)) {
2632 385
            $discriminatorValue = $class->defaultDiscriminatorValue;
2633
        }
2634 385
2635
        if ($discriminatorValue !== null) {
2636
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2637 385
2638 385
            $class = $this->dm->getClassMetadata($className);
2639 19
2640 377
            unset($data[$class->discriminatorField]);
2641 2
        }
2642
2643
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2644 385
            $document = $class->newInstance();
2645 20
            $this->hydratorFactory->hydrate($document, $data, $hints);
2646 18
            return $document;
2647 20
        }
2648
2649 20
        $isManagedObject = false;
2650
        if (! $class->isQueryResultDocument) {
2651 20
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2652
            $serializedId = serialize($id);
2653
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2654 385
        }
2655 2
2656 2
        if ($isManagedObject) {
2657 2
            $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...
2658
            $oid = spl_object_hash($document);
2659
            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...
2660 384
                $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...
2661 384
                $overrideLocalValues = true;
2662 381
                if ($document instanceof NotifyPropertyChanged) {
2663 381
                    $document->addPropertyChangedListener($this);
2664 381
                }
2665
            } else {
2666
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2667 384
            }
2668 104
            if ($overrideLocalValues) {
2669 104
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2670 104
                $this->originalDocumentData[$oid] = $data;
2671 14
            }
2672 14
        } else {
2673 14
            if ($document === null) {
2674 14
                $document = $class->newInstance();
2675
            }
2676
2677 96
            if (! $class->isQueryResultDocument) {
2678
                $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...
2679 104
                $oid = spl_object_hash($document);
2680 53
                $this->documentStates[$oid] = self::STATE_MANAGED;
2681 104
                $this->identityMap[$class->name][$serializedId] = $document;
2682
            }
2683
2684 345
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2685 345
2686
            if (! $class->isQueryResultDocument) {
2687
                $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...
2688 345
            }
2689 341
        }
2690 341
2691 341
        return $document;
2692 341
    }
2693
2694
    /**
2695 345
     * Initializes (loads) an uninitialized persistent collection of a document.
2696
     *
2697 345
     * @param PersistentCollectionInterface $collection The collection to initialize.
2698 341
     */
2699
    public function loadCollection(PersistentCollectionInterface $collection)
2700
    {
2701
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2702 384
        $this->lifecycleEventManager->postCollectionLoad($collection);
2703
    }
2704
2705
    /**
2706
     * Gets the identity map of the UnitOfWork.
2707
     *
2708
     * @return array
2709
     */
2710 163
    public function getIdentityMap()
2711
    {
2712 163
        return $this->identityMap;
2713 163
    }
2714 163
2715
    /**
2716
     * Gets the original data of a document. The original data is the data that was
2717
     * present at the time the document was reconstituted from the database.
2718
     *
2719
     * @param object $document
2720
     * @return array
2721
     */
2722
    public function getOriginalDocumentData($document)
2723
    {
2724
        $oid = spl_object_hash($document);
2725
2726
        return $this->originalDocumentData[$oid] ?? array();
2727
    }
2728
2729
    /**
2730
     * @ignore
2731
     */
2732
    public function setOriginalDocumentData($document, array $data)
2733 1
    {
2734
        $oid = spl_object_hash($document);
2735 1
        $this->originalDocumentData[$oid] = $data;
2736 1
        unset($this->documentChangeSets[$oid]);
2737 1
    }
2738
2739
    /**
2740
     * INTERNAL:
2741
     * Sets a property value of the original data array of a document.
2742
     *
2743
     * @ignore
2744
     * @param string $oid
2745 58
     * @param string $property
2746
     * @param mixed $value
2747 58
     */
2748 58
    public function setOriginalDocumentProperty($oid, $property, $value)
2749 58
    {
2750 58
        $this->originalDocumentData[$oid][$property] = $value;
2751
    }
2752
2753
    /**
2754
     * Gets the identifier of a document.
2755
     *
2756
     * @param object $document
2757
     * @return mixed The identifier value
2758
     */
2759
    public function getDocumentIdentifier($document)
2760
    {
2761 3
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2762
    }
2763 3
2764 3
    /**
2765
     * Checks whether the UnitOfWork has any pending insertions.
2766
     *
2767
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2768
     */
2769
    public function hasPendingInsertions()
2770
    {
2771
        return ! empty($this->documentInsertions);
2772 418
    }
2773
2774 418
    /**
2775 418
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2776
     * number of documents in the identity map.
2777
     *
2778
     * @return integer
2779
     */
2780
    public function size()
2781
    {
2782
        $count = 0;
2783
        foreach ($this->identityMap as $documentSet) {
2784
            $count += count($documentSet);
2785
        }
2786
        return $count;
2787
    }
2788
2789
    /**
2790
     * INTERNAL:
2791
     * Registers a document as managed.
2792
     *
2793
     * TODO: This method assumes that $id is a valid PHP identifier for the
2794 2
     * document class. If the class expects its database identifier to be an
2795
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2796 2
     * document identifiers map will become inconsistent with the identity map.
2797 2
     * In the future, we may want to round-trip $id through a PHP and database
2798 2
     * conversion and throw an exception if it's inconsistent.
2799
     *
2800 2
     * @param object $document The document.
2801
     * @param array $id The identifier values.
2802
     * @param array $data The original document data.
2803
     */
2804
    public function registerManaged($document, $id, $data)
2805
    {
2806
        $oid = spl_object_hash($document);
2807
        $class = $this->dm->getClassMetadata(get_class($document));
2808
2809
        if ( ! $class->identifier || $id === null) {
2810
            $this->documentIdentifiers[$oid] = $oid;
2811
        } else {
2812
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2813
        }
2814
2815
        $this->documentStates[$oid] = self::STATE_MANAGED;
2816
        $this->originalDocumentData[$oid] = $data;
2817
        $this->addToIdentityMap($document);
2818 367
    }
2819
2820 367
    /**
2821 367
     * INTERNAL:
2822
     * Clears the property changeset of the document with the given OID.
2823 367
     *
2824 92
     * @param string $oid The document's OID.
2825
     */
2826 361
    public function clearDocumentChangeSet($oid)
2827
    {
2828
        $this->documentChangeSets[$oid] = array();
2829 367
    }
2830 367
2831 367
    /* PropertyChangedListener implementation */
2832 367
2833
    /**
2834
     * Notifies this UnitOfWork of a property change in a document.
2835
     *
2836
     * @param object $document The document that owns the property.
2837
     * @param string $propertyName The name of the property that changed.
2838
     * @param mixed $oldValue The old value of the property.
2839
     * @param mixed $newValue The new value of the property.
2840
     */
2841
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2842
    {
2843
        $oid = spl_object_hash($document);
2844
        $class = $this->dm->getClassMetadata(get_class($document));
2845
2846
        if ( ! isset($class->fieldMappings[$propertyName])) {
2847
            return; // ignore non-persistent fields
2848
        }
2849
2850
        // Update changeset and mark document for synchronization
2851
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2852
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2853
            $this->scheduleForDirtyCheck($document);
2854
        }
2855 2
    }
2856
2857 2
    /**
2858 2
     * Gets the currently scheduled document insertions in this UnitOfWork.
2859
     *
2860 2
     * @return array
2861 1
     */
2862
    public function getScheduledDocumentInsertions()
2863
    {
2864
        return $this->documentInsertions;
2865 2
    }
2866 2
2867 2
    /**
2868
     * Gets the currently scheduled document upserts in this UnitOfWork.
2869 2
     *
2870
     * @return array
2871
     */
2872
    public function getScheduledDocumentUpserts()
2873
    {
2874
        return $this->documentUpserts;
2875
    }
2876 2
2877
    /**
2878 2
     * Gets the currently scheduled document updates in this UnitOfWork.
2879
     *
2880
     * @return array
2881
     */
2882
    public function getScheduledDocumentUpdates()
2883
    {
2884
        return $this->documentUpdates;
2885
    }
2886 1
2887
    /**
2888 1
     * Gets the currently scheduled document deletions in this UnitOfWork.
2889
     *
2890
     * @return array
2891
     */
2892
    public function getScheduledDocumentDeletions()
2893
    {
2894
        return $this->documentDeletions;
2895
    }
2896 1
2897
    /**
2898 1
     * Get the currently scheduled complete collection deletions
2899
     *
2900
     * @return array
2901
     */
2902
    public function getScheduledCollectionDeletions()
2903
    {
2904
        return $this->collectionDeletions;
2905
    }
2906
2907
    /**
2908
     * Gets the currently scheduled collection inserts, updates and deletes.
2909
     *
2910
     * @return array
2911
     */
2912
    public function getScheduledCollectionUpdates()
2913
    {
2914
        return $this->collectionUpdates;
2915
    }
2916
2917
    /**
2918
     * Helper method to initialize a lazy loading proxy or persistent collection.
2919
     *
2920
     * @param object
2921
     * @return void
2922
     */
2923
    public function initializeObject($obj)
2924
    {
2925
        if ($obj instanceof Proxy) {
2926
            $obj->__load();
2927
        } elseif ($obj instanceof PersistentCollectionInterface) {
2928
            $obj->initialize();
2929
        }
2930
    }
2931
2932
    private function objToStr($obj)
2933
    {
2934
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2935
    }
2936
}
2937