Completed
Pull Request — master (#1713)
by
unknown
11:24 queued 02:45
created

UnitOfWork::getClassNameForAssociation()   C

Complexity

Conditions 8
Paths 30

Size

Total Lines 32
Code Lines 20

Duplication

Lines 5
Ratio 15.63 %

Code Coverage

Tests 19
CRAP Score 8.0079

Importance

Changes 0
Metric Value
dl 5
loc 32
ccs 19
cts 20
cp 0.95
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 20
nc 30
nop 2
crap 8.0079
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 1626
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
258
    {
259 1626
        $this->dm = $dm;
260 1626
        $this->evm = $evm;
261 1626
        $this->hydratorFactory = $hydratorFactory;
262 1626
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
263 1626
    }
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 1085
    public function getPersistenceBuilder()
272
    {
273 1085
        if ( ! $this->persistenceBuilder) {
274 1085
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
275
        }
276 1085
        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 176
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
288
    {
289 176
        $oid = spl_object_hash($document);
290 176
        $this->embeddedDocumentsRegistry[$oid] = $document;
291 176
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
292 176
    }
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 202
    public function getParentAssociation($document)
305
    {
306 202
        $oid = spl_object_hash($document);
307 202
        if ( ! isset($this->parentAssociations[$oid])) {
308 196
            return null;
309
        }
310 151
        return $this->parentAssociations[$oid];
311
    }
312
313
    /**
314
     * Get the document persister instance for the given document name
315
     *
316
     * @param string $documentName
317
     * @return Persisters\DocumentPersister
318
     */
319 1083
    public function getDocumentPersister($documentName)
320
    {
321 1083
        if ( ! isset($this->persisters[$documentName])) {
322 1070
            $class = $this->dm->getClassMetadata($documentName);
323 1070
            $pb = $this->getPersistenceBuilder();
324 1070
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
325
        }
326 1083
        return $this->persisters[$documentName];
327
    }
328
329
    /**
330
     * Get the collection persister instance.
331
     *
332
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
333
     */
334 1083
    public function getCollectionPersister()
335
    {
336 1083
        if ( ! isset($this->collectionPersister)) {
337 1083
            $pb = $this->getPersistenceBuilder();
338 1083
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
339
        }
340 1083
        return $this->collectionPersister;
341
    }
342
343
    /**
344
     * Set the document persister instance to use for the given document name
345
     *
346
     * @param string $documentName
347
     * @param Persisters\DocumentPersister $persister
348
     */
349 13
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
350
    {
351 13
        $this->persisters[$documentName] = $persister;
352 13
    }
353
354
    /**
355
     * Commits the UnitOfWork, executing all operations that have been postponed
356
     * up to this point. The state of all managed documents will be synchronized with
357
     * the database.
358
     *
359
     * The operations are executed in the following order:
360
     *
361
     * 1) All document insertions
362
     * 2) All document updates
363
     * 3) All document deletions
364
     *
365
     * @param object $document
366
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
367
     */
368 576
    public function commit($document = null, array $options = array())
369
    {
370
        // Raise preFlush
371 576
        if ($this->evm->hasListeners(Events::preFlush)) {
372
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
373
        }
374
375
        // Compute changes done since last commit.
376 576
        if ($document === null) {
377 571
            $this->computeChangeSets();
378 13
        } elseif (is_object($document)) {
379 12
            $this->computeSingleDocumentChangeSet($document);
380 1
        } elseif (is_array($document)) {
381 1
            foreach ($document as $object) {
382 1
                $this->computeSingleDocumentChangeSet($object);
383
            }
384
        }
385
386 574
        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...
387 244
            $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...
388 201
            $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...
389 187
            $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...
390 22
            $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...
391 22
            $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...
392 574
            $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...
393
        ) {
394 22
            return; // Nothing to do.
395
        }
396
397 571
        $this->commitsInProgress++;
398 571
        if ($this->commitsInProgress > 1) {
399
            @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...
400
        }
401
        try {
402 571
            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...
403 44
                foreach ($this->orphanRemovals as $removal) {
404 44
                    $this->remove($removal);
405
                }
406
            }
407
408
            // Raise onFlush
409 571
            if ($this->evm->hasListeners(Events::onFlush)) {
410 3
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
411
            }
412
413 570
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
414 86
                list($class, $documents) = $classAndDocuments;
415 86
                $this->executeUpserts($class, $documents, $options);
416
            }
417
418 570
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
419 495
                list($class, $documents) = $classAndDocuments;
420 495
                $this->executeInserts($class, $documents, $options);
421
            }
422
423 569
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
424 207
                list($class, $documents) = $classAndDocuments;
425 207
                $this->executeUpdates($class, $documents, $options);
426
            }
427
428 569
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
429 65
                list($class, $documents) = $classAndDocuments;
430 65
                $this->executeDeletions($class, $documents, $options);
431
            }
432
433
            // Raise postFlush
434 569
            if ($this->evm->hasListeners(Events::postFlush)) {
435
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
436
            }
437
438
            // Clear up
439 569
            $this->documentInsertions =
440 569
            $this->documentUpserts =
441 569
            $this->documentUpdates =
442 569
            $this->documentDeletions =
443 569
            $this->documentChangeSets =
444 569
            $this->collectionUpdates =
445 569
            $this->collectionDeletions =
446 569
            $this->visitedCollections =
447 569
            $this->scheduledForDirtyCheck =
448 569
            $this->orphanRemovals =
449 569
            $this->hasScheduledCollections = array();
450 569
        } finally {
451 571
            $this->commitsInProgress--;
452
        }
453 569
    }
454
455
    /**
456
     * Groups a list of scheduled documents by their class.
457
     *
458
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
459
     * @param bool $includeEmbedded
460
     * @return array Tuples of ClassMetadata and a corresponding array of objects
461
     */
462 570
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
463
    {
464 570
        if (empty($documents)) {
465 570
            return array();
466
        }
467 569
        $divided = array();
468 569
        $embeds = array();
469 569
        foreach ($documents as $oid => $d) {
470 569
            $className = get_class($d);
471 569
            if (isset($embeds[$className])) {
472 69
                continue;
473
            }
474 569
            if (isset($divided[$className])) {
475 158
                $divided[$className][1][$oid] = $d;
476 158
                continue;
477
            }
478 569
            $class = $this->dm->getClassMetadata($className);
479 569
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
480 153
                $embeds[$className] = true;
481 153
                continue;
482
            }
483 569
            if (empty($divided[$class->name])) {
484 569
                $divided[$class->name] = array($class, array($oid => $d));
485
            } else {
486 569
                $divided[$class->name][1][$oid] = $d;
487
            }
488
        }
489 569
        return $divided;
490
    }
491
492
    /**
493
     * Compute changesets of all documents scheduled for insertion.
494
     *
495
     * Embedded documents will not be processed.
496
     */
497 578 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...
498
    {
499 578
        foreach ($this->documentInsertions as $document) {
500 506
            $class = $this->dm->getClassMetadata(get_class($document));
501 506
            if ( ! $class->isEmbeddedDocument) {
502 506
                $this->computeChangeSet($class, $document);
503
            }
504
        }
505 577
    }
506
507
    /**
508
     * Compute changesets of all documents scheduled for upsert.
509
     *
510
     * Embedded documents will not be processed.
511
     */
512 577 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...
513
    {
514 577
        foreach ($this->documentUpserts as $document) {
515 85
            $class = $this->dm->getClassMetadata(get_class($document));
516 85
            if ( ! $class->isEmbeddedDocument) {
517 85
                $this->computeChangeSet($class, $document);
518
            }
519
        }
520 577
    }
521
522
    /**
523
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
524
     *
525
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
526
     * 2. Proxies are skipped.
527
     * 3. Only if document is properly managed.
528
     *
529
     * @param  object $document
530
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
531
     * @return void
532
     */
533 13
    private function computeSingleDocumentChangeSet($document)
534
    {
535 13
        $state = $this->getDocumentState($document);
536
537 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
538 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
539
        }
540
541 12
        $class = $this->dm->getClassMetadata(get_class($document));
542
543 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
544 10
            $this->persist($document);
545
        }
546
547
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
548 12
        $this->computeScheduleInsertsChangeSets();
549 12
        $this->computeScheduleUpsertsChangeSets();
550
551
        // Ignore uninitialized proxy objects
552 12
        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...
553
            return;
554
        }
555
556
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
557 12
        $oid = spl_object_hash($document);
558
559 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...
560 12
            && ! isset($this->documentUpserts[$oid])
561 12
            && ! isset($this->documentDeletions[$oid])
562 12
            && isset($this->documentStates[$oid])
563
        ) {
564 7
            $this->computeChangeSet($class, $document);
565
        }
566 12
    }
567
568
    /**
569
     * Gets the changeset for a document.
570
     *
571
     * @param object $document
572
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
573
     */
574 572
    public function getDocumentChangeSet($document)
575
    {
576 572
        $oid = spl_object_hash($document);
577 572
        if (isset($this->documentChangeSets[$oid])) {
578 568
            return $this->documentChangeSets[$oid];
579
        }
580 54
        return array();
581
    }
582
583
    /**
584
     * INTERNAL:
585
     * Sets the changeset for a document.
586
     *
587
     * @param object $document
588
     * @param array $changeset
589
     */
590 1
    public function setDocumentChangeSet($document, $changeset)
591
    {
592 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
593 1
    }
594
595
    /**
596
     * Get a documents actual data, flattening all the objects to arrays.
597
     *
598
     * @param object $document
599
     * @return array
600
     */
601 579
    public function getDocumentActualData($document)
602
    {
603 579
        $class = $this->dm->getClassMetadata(get_class($document));
604 579
        $actualData = array();
605 579
        foreach ($class->reflFields as $name => $refProp) {
606 579
            $mapping = $class->fieldMappings[$name];
607
            // skip not saved fields
608 579
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
609 27
                continue;
610
            }
611 579
            $value = $refProp->getValue($document);
612 579
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
613 579
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
614
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
615 370
                if ( ! $value instanceof Collection) {
616 139
                    $value = new ArrayCollection($value);
617
                }
618
619
                // Inject PersistentCollection
620 370
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
621 370
                $coll->setOwner($document, $mapping);
622 370
                $coll->setDirty( ! $value->isEmpty());
623 370
                $class->reflFields[$name]->setValue($document, $coll);
624 370
                $actualData[$name] = $coll;
625
            } else {
626 579
                $actualData[$name] = $value;
627
            }
628
        }
629 579
        return $actualData;
630
    }
631
632
    /**
633
     * Computes the changes that happened to a single document.
634
     *
635
     * Modifies/populates the following properties:
636
     *
637
     * {@link originalDocumentData}
638
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
639
     * then it was not fetched from the database and therefore we have no original
640
     * document data yet. All of the current document data is stored as the original document data.
641
     *
642
     * {@link documentChangeSets}
643
     * The changes detected on all properties of the document are stored there.
644
     * A change is a tuple array where the first entry is the old value and the second
645
     * entry is the new value of the property. Changesets are used by persisters
646
     * to INSERT/UPDATE the persistent document state.
647
     *
648
     * {@link documentUpdates}
649
     * If the document is already fully MANAGED (has been fetched from the database before)
650
     * and any changes to its properties are detected, then a reference to the document is stored
651
     * there to mark it for an update.
652
     *
653
     * @param ClassMetadata $class The class descriptor of the document.
654
     * @param object $document The document for which to compute the changes.
655
     */
656 575
    public function computeChangeSet(ClassMetadata $class, $document)
657
    {
658 575
        if ( ! $class->isInheritanceTypeNone()) {
659 174
            $class = $this->dm->getClassMetadata(get_class($document));
660
        }
661
662
        // Fire PreFlush lifecycle callbacks
663 575 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...
664 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
665
        }
666
667 575
        $this->computeOrRecomputeChangeSet($class, $document);
668 574
    }
669
670
    /**
671
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
672
     *
673
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
674
     * @param object $document
675
     * @param boolean $recompute
676
     */
677 575
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
678
    {
679 575
        $oid = spl_object_hash($document);
680 575
        $actualData = $this->getDocumentActualData($document);
681 575
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
682 575
        if ($isNewDocument) {
683
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
684
            // These result in an INSERT.
685 575
            $this->originalDocumentData[$oid] = $actualData;
686 575
            $changeSet = array();
687 575
            foreach ($actualData as $propName => $actualValue) {
688
                /* At this PersistentCollection shouldn't be here, probably it
689
                 * was cloned and its ownership must be fixed
690
                 */
691 575
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
692
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
693
                    $actualValue = $actualData[$propName];
694
                }
695
                // ignore inverse side of reference relationship
696 575 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...
697 183
                    continue;
698
                }
699 575
                $changeSet[$propName] = array(null, $actualValue);
700
            }
701 575
            $this->documentChangeSets[$oid] = $changeSet;
702
        } else {
703 263
            if ($class->isReadOnly) {
704 2
                return;
705
            }
706
            // Document is "fully" MANAGED: it was already fully persisted before
707
            // and we have a copy of the original data
708 261
            $originalData = $this->originalDocumentData[$oid];
709 261
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
710 261
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
711 2
                $changeSet = $this->documentChangeSets[$oid];
712
            } else {
713 261
                $changeSet = array();
714
            }
715
716 261
            foreach ($actualData as $propName => $actualValue) {
717
                // skip not saved fields
718 261
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
719
                    continue;
720
                }
721
722 261
                $orgValue = $originalData[$propName] ?? null;
723
724
                // skip if value has not changed
725 261
                if ($orgValue === $actualValue) {
726 260
                    if (!$actualValue instanceof PersistentCollectionInterface) {
727 260
                        continue;
728
                    }
729
730 180
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
731
                        // consider dirty collections as changed as well
732 156
                        continue;
733
                    }
734
                }
735
736
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
737 223
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
738 13
                    if ($orgValue !== null) {
739 8
                        $this->scheduleOrphanRemoval($orgValue);
740
                    }
741 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
742 13
                    continue;
743
                }
744
745
                // if owning side of reference-one relationship
746 217
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
747 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
748 1
                        $this->scheduleOrphanRemoval($orgValue);
749
                    }
750
751 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
752 13
                    continue;
753
                }
754
755 210
                if ($isChangeTrackingNotify) {
756 3
                    continue;
757
                }
758
759
                // ignore inverse side of reference relationship
760 208 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...
761 6
                    continue;
762
                }
763
764
                // Persistent collection was exchanged with the "originally"
765
                // created one. This can only mean it was cloned and replaced
766
                // on another document.
767 206
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
768 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
769
                }
770
771
                // if embed-many or reference-many relationship
772 206
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
773 99
                    $changeSet[$propName] = array($orgValue, $actualValue);
774
                    /* If original collection was exchanged with a non-empty value
775
                     * and $set will be issued, there is no need to $unset it first
776
                     */
777 99
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
778 19
                        continue;
779
                    }
780 86
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
781 15
                        $this->scheduleCollectionDeletion($orgValue);
782
                    }
783 86
                    continue;
784
                }
785
786
                // skip equivalent date values
787 135
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
788 37
                    $dateType = Type::getType('date');
789 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
790 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
791
792 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...
793 30
                        continue;
794
                    }
795
                }
796
797
                // regular field
798 118
                $changeSet[$propName] = array($orgValue, $actualValue);
799
            }
800 261
            if ($changeSet) {
801 212
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
802 15
                    ? $changeSet + $this->documentChangeSets[$oid]
803 210
                    : $changeSet;
804
805 212
                $this->originalDocumentData[$oid] = $actualData;
806 212
                $this->scheduleForUpdate($document);
807
            }
808
        }
809
810
        // Look for changes in associations of the document
811 575
        $associationMappings = array_filter(
812 575
            $class->associationMappings,
813
            function ($assoc) { return empty($assoc['notSaved']); }
814
        );
815
816 575
        foreach ($associationMappings as $mapping) {
817 436
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
818
819 436
            if ($value === null) {
820 299
                continue;
821
            }
822
823 423
            $this->computeAssociationChanges($document, $mapping, $value);
824
825 422
            if (isset($mapping['reference'])) {
826 319
                continue;
827
            }
828
829 321
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
830
831 321
            foreach ($values as $obj) {
832 157
                $oid2 = spl_object_hash($obj);
833
834 157
                if (isset($this->documentChangeSets[$oid2])) {
835 155
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
836
                        // instance of $value is the same as it was previously otherwise there would be
837
                        // change set already in place
838 34
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
839
                    }
840
841 155
                    if ( ! $isNewDocument) {
842 64
                        $this->scheduleForUpdate($document);
843
                    }
844
845 321
                    break;
846
                }
847
            }
848
        }
849 574
    }
850
851
    /**
852
     * Computes all the changes that have been done to documents and collections
853
     * since the last commit and stores these changes in the _documentChangeSet map
854
     * temporarily for access by the persisters, until the UoW commit is finished.
855
     */
856 574
    public function computeChangeSets()
857
    {
858 574
        $this->computeScheduleInsertsChangeSets();
859 573
        $this->computeScheduleUpsertsChangeSets();
860
861
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
862 573
        foreach ($this->identityMap as $className => $documents) {
863 573
            $class = $this->dm->getClassMetadata($className);
864 573
            if ($class->isEmbeddedDocument) {
865
                /* we do not want to compute changes to embedded documents up front
866
                 * in case embedded document was replaced and its changeset
867
                 * would corrupt data. Embedded documents' change set will
868
                 * be calculated by reachability from owning document.
869
                 */
870 148
                continue;
871
            }
872
873
            // If change tracking is explicit or happens through notification, then only compute
874
            // changes on document of that type that are explicitly marked for synchronization.
875
            switch (true) {
876 573
                case ($class->isChangeTrackingDeferredImplicit()):
877 572
                    $documentsToProcess = $documents;
878 572
                    break;
879
880 4
                case (isset($this->scheduledForDirtyCheck[$className])):
881 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
882 3
                    break;
883
884
                default:
885 4
                    $documentsToProcess = array();
886
887
            }
888
889 573
            foreach ($documentsToProcess as $document) {
890
                // Ignore uninitialized proxy objects
891 569
                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...
892 9
                    continue;
893
                }
894
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
895 569
                $oid = spl_object_hash($document);
896 569 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...
897 569
                    && ! isset($this->documentUpserts[$oid])
898 569
                    && ! isset($this->documentDeletions[$oid])
899 569
                    && isset($this->documentStates[$oid])
900
                ) {
901 573
                    $this->computeChangeSet($class, $document);
902
                }
903
            }
904
        }
905 573
    }
906
907
    /**
908
     * Computes the changes of an association.
909
     *
910
     * @param object $parentDocument
911
     * @param array $assoc
912
     * @param mixed $value The value of the association.
913
     * @throws \InvalidArgumentException
914
     */
915 423
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
916
    {
917 423
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
918 423
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
919 423
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
920
921 423
        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...
922 7
            return;
923
        }
924
925 422
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
926 226
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
927 222
                $this->scheduleCollectionUpdate($value);
928
            }
929 226
            $topmostOwner = $this->getOwningDocument($value->getOwner());
930 226
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
931 226
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
932 121
                $value->initialize();
933 121
                foreach ($value->getDeletedDocuments() as $orphan) {
934 20
                    $this->scheduleOrphanRemoval($orphan);
935
                }
936
            }
937
        }
938
939
        // Look through the documents, and in any of their associations,
940
        // for transient (new) documents, recursively. ("Persistence by reachability")
941
        // Unwrap. Uninitialized collections will simply be empty.
942 422
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
943
944 422
        $count = 0;
945 422
        foreach ($unwrappedValue as $key => $entry) {
946 335
            if ( ! is_object($entry)) {
947 1
                throw new \InvalidArgumentException(
948 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
949
                );
950
            }
951
952 334
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
953
954 334
            $state = $this->getDocumentState($entry, self::STATE_NEW);
955
956
            // Handle "set" strategy for multi-level hierarchy
957 334
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
958 334
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
959
960 334
            $count++;
961
962
            switch ($state) {
963 334
                case self::STATE_NEW:
964 52
                    if ( ! $assoc['isCascadePersist']) {
965
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
966
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
967
                            . ' Explicitly persist the new document or configure cascading persist operations'
968
                            . ' on the relationship.');
969
                    }
970
971 52
                    $this->persistNew($targetClass, $entry);
972 52
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
973 52
                    $this->computeChangeSet($targetClass, $entry);
974 52
                    break;
975
976 330
                case self::STATE_MANAGED:
977 330
                    if ($targetClass->isEmbeddedDocument) {
978 149
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
979 149
                        if ($knownParent && $knownParent !== $parentDocument) {
980 6
                            $entry = clone $entry;
981 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
982 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
983 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
984 3
                                $poid = spl_object_hash($parentDocument);
985 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
986 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
987
                                }
988
                            } else {
989
                                // must use unwrapped value to not trigger orphan removal
990 4
                                $unwrappedValue[$key] = $entry;
991
                            }
992 6
                            $this->persistNew($targetClass, $entry);
993
                        }
994 149
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
995 149
                        $this->computeChangeSet($targetClass, $entry);
996
                    }
997 330
                    break;
998
999 1
                case self::STATE_REMOVED:
1000
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1001
                    // and remove the element from Collection.
1002 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1003
                        unset($value[$key]);
1004
                    }
1005 1
                    break;
1006
1007
                case self::STATE_DETACHED:
1008
                    // Can actually not happen right now as we assume STATE_NEW,
1009
                    // so the exception will be raised from the DBAL layer (constraint violation).
1010
                    throw new \InvalidArgumentException('A detached document was found through a '
1011
                        . 'relationship during cascading a persist operation.');
1012
1013 334
                default:
1014
                    // MANAGED associated documents are already taken into account
1015
                    // during changeset calculation anyway, since they are in the identity map.
1016
1017
            }
1018
        }
1019 421
    }
1020
1021
    /**
1022
     * INTERNAL:
1023
     * Computes the changeset of an individual document, independently of the
1024
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1025
     *
1026
     * The passed document must be a managed document. If the document already has a change set
1027
     * because this method is invoked during a commit cycle then the change sets are added.
1028
     * whereby changes detected in this method prevail.
1029
     *
1030
     * @ignore
1031
     * @param ClassMetadata $class The class descriptor of the document.
1032
     * @param object $document The document for which to (re)calculate the change set.
1033
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1034
     */
1035 16
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1036
    {
1037
        // Ignore uninitialized proxy objects
1038 16
        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...
1039 1
            return;
1040
        }
1041
1042 15
        $oid = spl_object_hash($document);
1043
1044 15
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1045
            throw new \InvalidArgumentException('Document must be managed.');
1046
        }
1047
1048 15
        if ( ! $class->isInheritanceTypeNone()) {
1049
            $class = $this->dm->getClassMetadata(get_class($document));
1050
        }
1051
1052 15
        $this->computeOrRecomputeChangeSet($class, $document, true);
1053 15
    }
1054
1055
    /**
1056
     * @param ClassMetadata $class
1057
     * @param object $document
1058
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1059
     */
1060 605
    private function persistNew(ClassMetadata $class, $document)
1061
    {
1062 605
        $this->lifecycleEventManager->prePersist($class, $document);
1063 605
        $oid = spl_object_hash($document);
1064 605
        $upsert = false;
1065 605
        if ($class->identifier) {
1066 605
            $idValue = $class->getIdentifierValue($document);
1067 605
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1068
1069 605
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1070 3
                throw new \InvalidArgumentException(sprintf(
1071 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1072 3
                    get_class($document)
1073
                ));
1074
            }
1075
1076 604
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', $idValue)) {
1077 1
                throw new \InvalidArgumentException(sprintf(
1078 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1079 1
                    get_class($document)
1080
                ));
1081
            }
1082
1083 603
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1084 524
                $idValue = $class->idGenerator->generate($this->dm, $document);
1085 524
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1086 524
                $class->setIdentifierValue($document, $idValue);
1087
            }
1088
1089 603
            $this->documentIdentifiers[$oid] = $idValue;
1090
        } else {
1091
            // this is for embedded documents without identifiers
1092 129
            $this->documentIdentifiers[$oid] = $oid;
1093
        }
1094
1095 603
        $this->documentStates[$oid] = self::STATE_MANAGED;
1096
1097 603
        if ($upsert) {
1098 89
            $this->scheduleForUpsert($class, $document);
1099
        } else {
1100 532
            $this->scheduleForInsert($class, $document);
1101
        }
1102 603
    }
1103
1104
    /**
1105
     * Executes all document insertions for documents of the specified type.
1106
     *
1107
     * @param ClassMetadata $class
1108
     * @param array $documents Array of documents to insert
1109
     * @param array $options Array of options to be used with batchInsert()
1110
     */
1111 495 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...
1112
    {
1113 495
        $persister = $this->getDocumentPersister($class->name);
1114
1115 495
        foreach ($documents as $oid => $document) {
1116 495
            $persister->addInsert($document);
1117 495
            unset($this->documentInsertions[$oid]);
1118
        }
1119
1120 495
        $persister->executeInserts($options);
1121
1122 494
        foreach ($documents as $document) {
1123 494
            $this->lifecycleEventManager->postPersist($class, $document);
1124
        }
1125 494
    }
1126
1127
    /**
1128
     * Executes all document upserts for documents of the specified type.
1129
     *
1130
     * @param ClassMetadata $class
1131
     * @param array $documents Array of documents to upsert
1132
     * @param array $options Array of options to be used with batchInsert()
1133
     */
1134 86 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...
1135
    {
1136 86
        $persister = $this->getDocumentPersister($class->name);
1137
1138
1139 86
        foreach ($documents as $oid => $document) {
1140 86
            $persister->addUpsert($document);
1141 86
            unset($this->documentUpserts[$oid]);
1142
        }
1143
1144 86
        $persister->executeUpserts($options);
1145
1146 86
        foreach ($documents as $document) {
1147 86
            $this->lifecycleEventManager->postPersist($class, $document);
1148
        }
1149 86
    }
1150
1151
    /**
1152
     * Executes all document updates for documents of the specified type.
1153
     *
1154
     * @param Mapping\ClassMetadata $class
1155
     * @param array $documents Array of documents to update
1156
     * @param array $options Array of options to be used with update()
1157
     */
1158 207
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1159
    {
1160 207
        if ($class->isReadOnly) {
1161
            return;
1162
        }
1163
1164 207
        $className = $class->name;
1165 207
        $persister = $this->getDocumentPersister($className);
1166
1167 207
        foreach ($documents as $oid => $document) {
1168 207
            $this->lifecycleEventManager->preUpdate($class, $document);
1169
1170 207
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1171 206
                $persister->update($document, $options);
1172
            }
1173
1174 203
            unset($this->documentUpdates[$oid]);
1175
1176 203
            $this->lifecycleEventManager->postUpdate($class, $document);
1177
        }
1178 203
    }
1179
1180
    /**
1181
     * Executes all document deletions for documents of the specified type.
1182
     *
1183
     * @param ClassMetadata $class
1184
     * @param array $documents Array of documents to delete
1185
     * @param array $options Array of options to be used with remove()
1186
     */
1187 65
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1188
    {
1189 65
        $persister = $this->getDocumentPersister($class->name);
1190
1191 65
        foreach ($documents as $oid => $document) {
1192 65
            if ( ! $class->isEmbeddedDocument) {
1193 33
                $persister->delete($document, $options);
1194
            }
1195
            unset(
1196 63
                $this->documentDeletions[$oid],
1197 63
                $this->documentIdentifiers[$oid],
1198 63
                $this->originalDocumentData[$oid]
1199
            );
1200
1201
            // Clear snapshot information for any referenced PersistentCollection
1202
            // http://www.doctrine-project.org/jira/browse/MODM-95
1203 63
            foreach ($class->associationMappings as $fieldMapping) {
1204 38
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1205 24
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1206 24
                    if ($value instanceof PersistentCollectionInterface) {
1207 38
                        $value->clearSnapshot();
1208
                    }
1209
                }
1210
            }
1211
1212
            // Document with this $oid after deletion treated as NEW, even if the $oid
1213
            // is obtained by a new document because the old one went out of scope.
1214 63
            $this->documentStates[$oid] = self::STATE_NEW;
1215
1216 63
            $this->lifecycleEventManager->postRemove($class, $document);
1217
        }
1218 63
    }
1219
1220
    /**
1221
     * Schedules a document for insertion into the database.
1222
     * If the document already has an identifier, it will be added to the
1223
     * identity map.
1224
     *
1225
     * @param ClassMetadata $class
1226
     * @param object $document The document to schedule for insertion.
1227
     * @throws \InvalidArgumentException
1228
     */
1229 535
    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...
1230
    {
1231 535
        $oid = spl_object_hash($document);
1232
1233 535
        if (isset($this->documentUpdates[$oid])) {
1234
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1235
        }
1236 535
        if (isset($this->documentDeletions[$oid])) {
1237
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1238
        }
1239 535
        if (isset($this->documentInsertions[$oid])) {
1240
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1241
        }
1242
1243 535
        $this->documentInsertions[$oid] = $document;
1244
1245 535
        if (isset($this->documentIdentifiers[$oid])) {
1246 532
            $this->addToIdentityMap($document);
1247
        }
1248 535
    }
1249
1250
    /**
1251
     * Schedules a document for upsert into the database and adds it to the
1252
     * identity map
1253
     *
1254
     * @param ClassMetadata $class
1255
     * @param object $document The document to schedule for upsert.
1256
     * @throws \InvalidArgumentException
1257
     */
1258 92
    public function scheduleForUpsert(ClassMetadata $class, $document)
1259
    {
1260 92
        $oid = spl_object_hash($document);
1261
1262 92
        if ($class->isEmbeddedDocument) {
1263
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1264
        }
1265 92
        if (isset($this->documentUpdates[$oid])) {
1266
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1267
        }
1268 92
        if (isset($this->documentDeletions[$oid])) {
1269
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1270
        }
1271 92
        if (isset($this->documentUpserts[$oid])) {
1272
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1273
        }
1274
1275 92
        $this->documentUpserts[$oid] = $document;
1276 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1277 92
        $this->addToIdentityMap($document);
1278 92
    }
1279
1280
    /**
1281
     * Checks whether a document is scheduled for insertion.
1282
     *
1283
     * @param object $document
1284
     * @return boolean
1285
     */
1286 91
    public function isScheduledForInsert($document)
1287
    {
1288 91
        return isset($this->documentInsertions[spl_object_hash($document)]);
1289
    }
1290
1291
    /**
1292
     * Checks whether a document is scheduled for upsert.
1293
     *
1294
     * @param object $document
1295
     * @return boolean
1296
     */
1297 5
    public function isScheduledForUpsert($document)
1298
    {
1299 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1300
    }
1301
1302
    /**
1303
     * Schedules a document for being updated.
1304
     *
1305
     * @param object $document The document to schedule for being updated.
1306
     * @throws \InvalidArgumentException
1307
     */
1308 213
    public function scheduleForUpdate($document)
1309
    {
1310 213
        $oid = spl_object_hash($document);
1311 213
        if ( ! isset($this->documentIdentifiers[$oid])) {
1312
            throw new \InvalidArgumentException('Document has no identity.');
1313
        }
1314
1315 213
        if (isset($this->documentDeletions[$oid])) {
1316
            throw new \InvalidArgumentException('Document is removed.');
1317
        }
1318
1319 213
        if ( ! isset($this->documentUpdates[$oid])
1320 213
            && ! isset($this->documentInsertions[$oid])
1321 213
            && ! isset($this->documentUpserts[$oid])) {
1322 212
            $this->documentUpdates[$oid] = $document;
1323
        }
1324 213
    }
1325
1326
    /**
1327
     * Checks whether a document is registered as dirty in the unit of work.
1328
     * Note: Is not very useful currently as dirty documents are only registered
1329
     * at commit time.
1330
     *
1331
     * @param object $document
1332
     * @return boolean
1333
     */
1334 15
    public function isScheduledForUpdate($document)
1335
    {
1336 15
        return isset($this->documentUpdates[spl_object_hash($document)]);
1337
    }
1338
1339 1
    public function isScheduledForDirtyCheck($document)
1340
    {
1341 1
        $class = $this->dm->getClassMetadata(get_class($document));
1342 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1343
    }
1344
1345
    /**
1346
     * INTERNAL:
1347
     * Schedules a document for deletion.
1348
     *
1349
     * @param object $document
1350
     */
1351 70
    public function scheduleForDelete($document)
1352
    {
1353 70
        $oid = spl_object_hash($document);
1354
1355 70
        if (isset($this->documentInsertions[$oid])) {
1356 1
            if ($this->isInIdentityMap($document)) {
1357 1
                $this->removeFromIdentityMap($document);
1358
            }
1359 1
            unset($this->documentInsertions[$oid]);
1360 1
            return; // document has not been persisted yet, so nothing more to do.
1361
        }
1362
1363 69
        if ( ! $this->isInIdentityMap($document)) {
1364 2
            return; // ignore
1365
        }
1366
1367 68
        $this->removeFromIdentityMap($document);
1368 68
        $this->documentStates[$oid] = self::STATE_REMOVED;
1369
1370 68
        if (isset($this->documentUpdates[$oid])) {
1371
            unset($this->documentUpdates[$oid]);
1372
        }
1373 68
        if ( ! isset($this->documentDeletions[$oid])) {
1374 68
            $this->documentDeletions[$oid] = $document;
1375
        }
1376 68
    }
1377
1378
    /**
1379
     * Checks whether a document is registered as removed/deleted with the unit
1380
     * of work.
1381
     *
1382
     * @param object $document
1383
     * @return boolean
1384
     */
1385 8
    public function isScheduledForDelete($document)
1386
    {
1387 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1388
    }
1389
1390
    /**
1391
     * Checks whether a document is scheduled for insertion, update or deletion.
1392
     *
1393
     * @param $document
1394
     * @return boolean
1395
     */
1396 225
    public function isDocumentScheduled($document)
1397
    {
1398 225
        $oid = spl_object_hash($document);
1399 225
        return isset($this->documentInsertions[$oid]) ||
1400 111
            isset($this->documentUpserts[$oid]) ||
1401 102
            isset($this->documentUpdates[$oid]) ||
1402 225
            isset($this->documentDeletions[$oid]);
1403
    }
1404
1405
    /**
1406
     * INTERNAL:
1407
     * Registers a document in the identity map.
1408
     *
1409
     * Note that documents in a hierarchy are registered with the class name of
1410
     * the root document. Identifiers are serialized before being used as array
1411
     * keys to allow differentiation of equal, but not identical, values.
1412
     *
1413
     * @ignore
1414
     * @param object $document  The document to register.
1415
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1416
     *                  the document in question is already managed.
1417
     */
1418 635
    public function addToIdentityMap($document)
1419
    {
1420 635
        $class = $this->dm->getClassMetadata(get_class($document));
1421 635
        $id = $this->getIdForIdentityMap($document);
1422
1423 635
        if (isset($this->identityMap[$class->name][$id])) {
1424 57
            return false;
1425
        }
1426
1427 635
        $this->identityMap[$class->name][$id] = $document;
1428
1429 635
        if ($document instanceof NotifyPropertyChanged &&
1430 635
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1431 3
            $document->addPropertyChangedListener($this);
1432
        }
1433
1434 635
        return true;
1435
    }
1436
1437
    /**
1438
     * Gets the state of a document with regard to the current unit of work.
1439
     *
1440
     * @param object   $document
1441
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1442
     *                         This parameter can be set to improve performance of document state detection
1443
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1444
     *                         is either known or does not matter for the caller of the method.
1445
     * @return int The document state.
1446
     */
1447 608
    public function getDocumentState($document, $assume = null)
1448
    {
1449 608
        $oid = spl_object_hash($document);
1450
1451 608
        if (isset($this->documentStates[$oid])) {
1452 375
            return $this->documentStates[$oid];
1453
        }
1454
1455 608
        $class = $this->dm->getClassMetadata(get_class($document));
1456
1457 608
        if ($class->isEmbeddedDocument) {
1458 162
            return self::STATE_NEW;
1459
        }
1460
1461 605
        if ($assume !== null) {
1462 602
            return $assume;
1463
        }
1464
1465
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1466
         * known. Note that you cannot remember the NEW or DETACHED state in
1467
         * _documentStates since the UoW does not hold references to such
1468
         * objects and the object hash can be reused. More generally, because
1469
         * the state may "change" between NEW/DETACHED without the UoW being
1470
         * aware of it.
1471
         */
1472 4
        $id = $class->getIdentifierObject($document);
1473
1474 4
        if ($id === null) {
1475 3
            return self::STATE_NEW;
1476
        }
1477
1478
        // Check for a version field, if available, to avoid a DB lookup.
1479 2
        if ($class->isVersioned) {
1480
            return $class->getFieldValue($document, $class->versionField)
1481
                ? self::STATE_DETACHED
1482
                : self::STATE_NEW;
1483
        }
1484
1485
        // Last try before DB lookup: check the identity map.
1486 2
        if ($this->tryGetById($id, $class)) {
1487 1
            return self::STATE_DETACHED;
1488
        }
1489
1490
        // DB lookup
1491 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1492 1
            return self::STATE_DETACHED;
1493
        }
1494
1495 1
        return self::STATE_NEW;
1496
    }
1497
1498
    /**
1499
     * INTERNAL:
1500
     * Removes a document from the identity map. This effectively detaches the
1501
     * document from the persistence management of Doctrine.
1502
     *
1503
     * @ignore
1504
     * @param object $document
1505
     * @throws \InvalidArgumentException
1506
     * @return boolean
1507
     */
1508 82
    public function removeFromIdentityMap($document)
1509
    {
1510 82
        $oid = spl_object_hash($document);
1511
1512
        // Check if id is registered first
1513 82
        if ( ! isset($this->documentIdentifiers[$oid])) {
1514
            return false;
1515
        }
1516
1517 82
        $class = $this->dm->getClassMetadata(get_class($document));
1518 82
        $id = $this->getIdForIdentityMap($document);
1519
1520 82
        if (isset($this->identityMap[$class->name][$id])) {
1521 82
            unset($this->identityMap[$class->name][$id]);
1522 82
            $this->documentStates[$oid] = self::STATE_DETACHED;
1523 82
            return true;
1524
        }
1525
1526
        return false;
1527
    }
1528
1529
    /**
1530
     * INTERNAL:
1531
     * Gets a document in the identity map by its identifier hash.
1532
     *
1533
     * @ignore
1534
     * @param mixed         $id    Document identifier
1535
     * @param ClassMetadata $class Document class
1536
     * @return object
1537
     * @throws InvalidArgumentException if the class does not have an identifier
1538
     */
1539 38
    public function getById($id, ClassMetadata $class)
1540
    {
1541 38
        if ( ! $class->identifier) {
1542
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1543
        }
1544
1545 38
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1546
1547 38
        return $this->identityMap[$class->name][$serializedId];
1548
    }
1549
1550
    /**
1551
     * INTERNAL:
1552
     * Tries to get a document by its identifier hash. If no document is found
1553
     * for the given hash, FALSE is returned.
1554
     *
1555
     * @ignore
1556
     * @param mixed         $id    Document identifier
1557
     * @param ClassMetadata $class Document class
1558
     * @return mixed The found document or FALSE.
1559
     * @throws InvalidArgumentException if the class does not have an identifier
1560
     */
1561 298
    public function tryGetById($id, ClassMetadata $class)
1562
    {
1563 298
        if ( ! $class->identifier) {
1564
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1565
        }
1566
1567 298
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1568
1569 298
        return isset($this->identityMap[$class->name][$serializedId]) ?
1570 298
            $this->identityMap[$class->name][$serializedId] : false;
1571
    }
1572
1573
    /**
1574
     * Schedules a document for dirty-checking at commit-time.
1575
     *
1576
     * @param object $document The document to schedule for dirty-checking.
1577
     * @todo Rename: scheduleForSynchronization
1578
     */
1579 3
    public function scheduleForDirtyCheck($document)
1580
    {
1581 3
        $class = $this->dm->getClassMetadata(get_class($document));
1582 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1583 3
    }
1584
1585
    /**
1586
     * Checks whether a document is registered in the identity map.
1587
     *
1588
     * @param object $document
1589
     * @return boolean
1590
     */
1591 81
    public function isInIdentityMap($document)
1592
    {
1593 81
        $oid = spl_object_hash($document);
1594
1595 81
        if ( ! isset($this->documentIdentifiers[$oid])) {
1596 6
            return false;
1597
        }
1598
1599 79
        $class = $this->dm->getClassMetadata(get_class($document));
1600 79
        $id = $this->getIdForIdentityMap($document);
1601
1602 79
        return isset($this->identityMap[$class->name][$id]);
1603
    }
1604
1605
    /**
1606
     * @param object $document
1607
     * @return string
1608
     */
1609 635
    private function getIdForIdentityMap($document)
1610
    {
1611 635
        $class = $this->dm->getClassMetadata(get_class($document));
1612
1613 635
        if ( ! $class->identifier) {
1614 132
            $id = spl_object_hash($document);
1615
        } else {
1616 634
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1617 634
            $id = serialize($class->getDatabaseIdentifierValue($id));
1618
        }
1619
1620 635
        return $id;
1621
    }
1622
1623
    /**
1624
     * INTERNAL:
1625
     * Checks whether an identifier exists in the identity map.
1626
     *
1627
     * @ignore
1628
     * @param string $id
1629
     * @param string $rootClassName
1630
     * @return boolean
1631
     */
1632
    public function containsId($id, $rootClassName)
1633
    {
1634
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1635
    }
1636
1637
    /**
1638
     * Persists a document as part of the current unit of work.
1639
     *
1640
     * @param object $document The document to persist.
1641
     * @throws MongoDBException If trying to persist MappedSuperclass.
1642
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1643
     */
1644 605
    public function persist($document)
1645
    {
1646 605
        $class = $this->dm->getClassMetadata(get_class($document));
1647 605
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1648 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1649
        }
1650 604
        $visited = array();
1651 604
        $this->doPersist($document, $visited);
1652 600
    }
1653
1654
    /**
1655
     * Saves a document as part of the current unit of work.
1656
     * This method is internally called during save() cascades as it tracks
1657
     * the already visited documents to prevent infinite recursions.
1658
     *
1659
     * NOTE: This method always considers documents that are not yet known to
1660
     * this UnitOfWork as NEW.
1661
     *
1662
     * @param object $document The document to persist.
1663
     * @param array $visited The already visited documents.
1664
     * @throws \InvalidArgumentException
1665
     * @throws MongoDBException
1666
     */
1667 604
    private function doPersist($document, array &$visited)
1668
    {
1669 604
        $oid = spl_object_hash($document);
1670 604
        if (isset($visited[$oid])) {
1671 25
            return; // Prevent infinite recursion
1672
        }
1673
1674 604
        $visited[$oid] = $document; // Mark visited
1675
1676 604
        $class = $this->dm->getClassMetadata(get_class($document));
1677
1678 604
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1679
        switch ($documentState) {
1680 604
            case self::STATE_MANAGED:
1681
                // Nothing to do, except if policy is "deferred explicit"
1682 58
                if ($class->isChangeTrackingDeferredExplicit()) {
1683
                    $this->scheduleForDirtyCheck($document);
1684
                }
1685 58
                break;
1686 604
            case self::STATE_NEW:
1687 604
                $this->persistNew($class, $document);
1688 602
                break;
1689
1690 2
            case self::STATE_REMOVED:
1691
                // Document becomes managed again
1692 2
                unset($this->documentDeletions[$oid]);
1693
1694 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1695 2
                break;
1696
1697
            case self::STATE_DETACHED:
1698
                throw new \InvalidArgumentException(
1699
                    'Behavior of persist() for a detached document is not yet defined.');
1700
1701
            default:
1702
                throw MongoDBException::invalidDocumentState($documentState);
1703
        }
1704
1705 602
        $this->cascadePersist($document, $visited);
1706 600
    }
1707
1708
    /**
1709
     * Deletes a document as part of the current unit of work.
1710
     *
1711
     * @param object $document The document to remove.
1712
     */
1713 69
    public function remove($document)
1714
    {
1715 69
        $visited = array();
1716 69
        $this->doRemove($document, $visited);
1717 69
    }
1718
1719
    /**
1720
     * Deletes a document as part of the current unit of work.
1721
     *
1722
     * This method is internally called during delete() cascades as it tracks
1723
     * the already visited documents to prevent infinite recursions.
1724
     *
1725
     * @param object $document The document to delete.
1726
     * @param array $visited The map of the already visited documents.
1727
     * @throws MongoDBException
1728
     */
1729 69
    private function doRemove($document, array &$visited)
1730
    {
1731 69
        $oid = spl_object_hash($document);
1732 69
        if (isset($visited[$oid])) {
1733 1
            return; // Prevent infinite recursion
1734
        }
1735
1736 69
        $visited[$oid] = $document; // mark visited
1737
1738
        /* Cascade first, because scheduleForDelete() removes the entity from
1739
         * the identity map, which can cause problems when a lazy Proxy has to
1740
         * be initialized for the cascade operation.
1741
         */
1742 69
        $this->cascadeRemove($document, $visited);
1743
1744 69
        $class = $this->dm->getClassMetadata(get_class($document));
1745 69
        $documentState = $this->getDocumentState($document);
1746
        switch ($documentState) {
1747 69
            case self::STATE_NEW:
1748 69
            case self::STATE_REMOVED:
1749
                // nothing to do
1750
                break;
1751 69
            case self::STATE_MANAGED:
1752 69
                $this->lifecycleEventManager->preRemove($class, $document);
1753 69
                $this->scheduleForDelete($document);
1754 69
                break;
1755
            case self::STATE_DETACHED:
1756
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1757
            default:
1758
                throw MongoDBException::invalidDocumentState($documentState);
1759
        }
1760 69
    }
1761
1762
    /**
1763
     * Merges the state of the given detached document into this UnitOfWork.
1764
     *
1765
     * @param object $document
1766
     * @return object The managed copy of the document.
1767
     */
1768 12
    public function merge($document)
1769
    {
1770 12
        $visited = array();
1771
1772 12
        return $this->doMerge($document, $visited);
1773
    }
1774
1775
    /**
1776
     * Executes a merge operation on a document.
1777
     *
1778
     * @param object      $document
1779
     * @param array       $visited
1780
     * @param object|null $prevManagedCopy
1781
     * @param array|null  $assoc
1782
     *
1783
     * @return object The managed copy of the document.
1784
     *
1785
     * @throws InvalidArgumentException If the entity instance is NEW.
1786
     * @throws LockException If the document uses optimistic locking through a
1787
     *                       version attribute and the version check against the
1788
     *                       managed copy fails.
1789
     */
1790 12
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1791
    {
1792 12
        $oid = spl_object_hash($document);
1793
1794 12
        if (isset($visited[$oid])) {
1795 1
            return $visited[$oid]; // Prevent infinite recursion
1796
        }
1797
1798 12
        $visited[$oid] = $document; // mark visited
1799
1800 12
        $class = $this->dm->getClassMetadata(get_class($document));
1801
1802
        /* First we assume DETACHED, although it can still be NEW but we can
1803
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1804
         * an identity, we need to fetch it from the DB anyway in order to
1805
         * merge. MANAGED documents are ignored by the merge operation.
1806
         */
1807 12
        $managedCopy = $document;
1808
1809 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1810 12
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1811
                $document->__load();
1812
            }
1813
1814 12
            $identifier = $class->getIdentifier();
1815
            // We always have one element in the identifier array but it might be null
1816 12
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1817 12
            $managedCopy = null;
1818
1819
            // Try to fetch document from the database
1820 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1821 12
                $managedCopy = $this->dm->find($class->name, $id);
1822
1823
                // Managed copy may be removed in which case we can't merge
1824 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1825
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1826
                }
1827
1828 12
                if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1829
                    $managedCopy->__load();
1830
                }
1831
            }
1832
1833 12
            if ($managedCopy === null) {
1834
                // Create a new managed instance
1835 4
                $managedCopy = $class->newInstance();
1836 4
                if ($id !== null) {
1837 3
                    $class->setIdentifierValue($managedCopy, $id);
1838
                }
1839 4
                $this->persistNew($class, $managedCopy);
1840
            }
1841
1842 12
            if ($class->isVersioned) {
1843
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1844
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1845
1846
                // Throw exception if versions don't match
1847
                if ($managedCopyVersion != $documentVersion) {
1848
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1849
                }
1850
            }
1851
1852
            // Merge state of $document into existing (managed) document
1853 12
            foreach ($class->reflClass->getProperties() as $prop) {
1854 12
                $name = $prop->name;
1855 12
                $prop->setAccessible(true);
1856 12
                if ( ! isset($class->associationMappings[$name])) {
1857 12
                    if ( ! $class->isIdentifier($name)) {
1858 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1859
                    }
1860
                } else {
1861 12
                    $assoc2 = $class->associationMappings[$name];
1862
1863 12
                    if ($assoc2['type'] === 'one') {
1864 6
                        $other = $prop->getValue($document);
1865
1866 6
                        if ($other === null) {
1867 2
                            $prop->setValue($managedCopy, null);
1868 5
                        } 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...
1869
                            // Do not merge fields marked lazy that have not been fetched
1870 1
                            continue;
1871 4
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1872
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1873
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1874
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1875
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1876
                                $relatedId = $targetClass->getIdentifierObject($other);
1877
1878
                                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...
1879
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1880
                                } else {
1881
                                    $other = $this
1882
                                        ->dm
1883
                                        ->getProxyFactory()
1884
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1885
                                    $this->registerManaged($other, $relatedId, array());
1886
                                }
1887
                            }
1888
1889 5
                            $prop->setValue($managedCopy, $other);
1890
                        }
1891
                    } else {
1892 10
                        $mergeCol = $prop->getValue($document);
1893
1894 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1895
                            /* Do not merge fields marked lazy that have not
1896
                             * been fetched. Keep the lazy persistent collection
1897
                             * of the managed copy.
1898
                             */
1899 3
                            continue;
1900
                        }
1901
1902 10
                        $managedCol = $prop->getValue($managedCopy);
1903
1904 10
                        if ( ! $managedCol) {
1905 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1906 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1907 1
                            $prop->setValue($managedCopy, $managedCol);
1908 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1909
                        }
1910
1911
                        /* Note: do not process association's target documents.
1912
                         * They will be handled during the cascade. Initialize
1913
                         * and, if necessary, clear $managedCol for now.
1914
                         */
1915 10
                        if ($assoc2['isCascadeMerge']) {
1916 10
                            $managedCol->initialize();
1917
1918
                            // If $managedCol differs from the merged collection, clear and set dirty
1919 10
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1920 3
                                $managedCol->unwrap()->clear();
1921 3
                                $managedCol->setDirty(true);
1922
1923 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1924
                                    $this->scheduleForDirtyCheck($managedCopy);
1925
                                }
1926
                            }
1927
                        }
1928
                    }
1929
                }
1930
1931 12
                if ($class->isChangeTrackingNotify()) {
1932
                    // Just treat all properties as changed, there is no other choice.
1933 12
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1934
                }
1935
            }
1936
1937 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1938
                $this->scheduleForDirtyCheck($document);
1939
            }
1940
        }
1941
1942 12
        if ($prevManagedCopy !== null) {
1943 5
            $assocField = $assoc['fieldName'];
1944 5
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1945
1946 5
            if ($assoc['type'] === 'one') {
1947 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1948
            } else {
1949 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1950
1951 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1952 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1953
                }
1954
            }
1955
        }
1956
1957
        // Mark the managed copy visited as well
1958 12
        $visited[spl_object_hash($managedCopy)] = true;
1959
1960 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1961
1962 12
        return $managedCopy;
1963
    }
1964
1965
    /**
1966
     * Detaches a document from the persistence management. It's persistence will
1967
     * no longer be managed by Doctrine.
1968
     *
1969
     * @param object $document The document to detach.
1970
     */
1971 11
    public function detach($document)
1972
    {
1973 11
        $visited = array();
1974 11
        $this->doDetach($document, $visited);
1975 11
    }
1976
1977
    /**
1978
     * Executes a detach operation on the given document.
1979
     *
1980
     * @param object $document
1981
     * @param array $visited
1982
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1983
     */
1984 16
    private function doDetach($document, array &$visited)
1985
    {
1986 16
        $oid = spl_object_hash($document);
1987 16
        if (isset($visited[$oid])) {
1988 3
            return; // Prevent infinite recursion
1989
        }
1990
1991 16
        $visited[$oid] = $document; // mark visited
1992
1993 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1994 16
            case self::STATE_MANAGED:
1995 16
                $this->removeFromIdentityMap($document);
1996 16
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1997 16
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1998 16
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1999 16
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2000 16
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2001 16
                break;
2002 3
            case self::STATE_NEW:
2003 3
            case self::STATE_DETACHED:
2004 3
                return;
2005
        }
2006
2007 16
        $this->cascadeDetach($document, $visited);
2008 16
    }
2009
2010
    /**
2011
     * Refreshes the state of the given document from the database, overwriting
2012
     * any local, unpersisted changes.
2013
     *
2014
     * @param object $document The document to refresh.
2015
     * @throws \InvalidArgumentException If the document is not MANAGED.
2016
     */
2017 21
    public function refresh($document)
2018
    {
2019 21
        $visited = array();
2020 21
        $this->doRefresh($document, $visited);
2021 20
    }
2022
2023
    /**
2024
     * Executes a refresh operation on a document.
2025
     *
2026
     * @param object $document The document to refresh.
2027
     * @param array $visited The already visited documents during cascades.
2028
     * @throws \InvalidArgumentException If the document is not MANAGED.
2029
     */
2030 21
    private function doRefresh($document, array &$visited)
2031
    {
2032 21
        $oid = spl_object_hash($document);
2033 21
        if (isset($visited[$oid])) {
2034
            return; // Prevent infinite recursion
2035
        }
2036
2037 21
        $visited[$oid] = $document; // mark visited
2038
2039 21
        $class = $this->dm->getClassMetadata(get_class($document));
2040
2041 21
        if ( ! $class->isEmbeddedDocument) {
2042 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2043 20
                $this->getDocumentPersister($class->name)->refresh($document);
2044
            } else {
2045 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2046
            }
2047
        }
2048
2049 20
        $this->cascadeRefresh($document, $visited);
2050 20
    }
2051
2052
    /**
2053
     * Cascades a refresh operation to associated documents.
2054
     *
2055
     * @param object $document
2056
     * @param array $visited
2057
     */
2058 20
    private function cascadeRefresh($document, array &$visited)
2059
    {
2060 20
        $class = $this->dm->getClassMetadata(get_class($document));
2061
2062 20
        $associationMappings = array_filter(
2063 20
            $class->associationMappings,
2064
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2065
        );
2066
2067 20
        foreach ($associationMappings as $mapping) {
2068 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2069 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2070 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2071
                    // Unwrap so that foreach() does not initialize
2072 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2073
                }
2074 15
                foreach ($relatedDocuments as $relatedDocument) {
2075 15
                    $this->doRefresh($relatedDocument, $visited);
2076
                }
2077 10
            } elseif ($relatedDocuments !== null) {
2078 15
                $this->doRefresh($relatedDocuments, $visited);
2079
            }
2080
        }
2081 20
    }
2082
2083
    /**
2084
     * Cascades a detach operation to associated documents.
2085
     *
2086
     * @param object $document
2087
     * @param array $visited
2088
     */
2089 16 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...
2090
    {
2091 16
        $class = $this->dm->getClassMetadata(get_class($document));
2092 16
        foreach ($class->fieldMappings as $mapping) {
2093 16
            if ( ! $mapping['isCascadeDetach']) {
2094 16
                continue;
2095
            }
2096 10
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2097 10
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2098 10
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2099
                    // Unwrap so that foreach() does not initialize
2100 7
                    $relatedDocuments = $relatedDocuments->unwrap();
2101
                }
2102 10
                foreach ($relatedDocuments as $relatedDocument) {
2103 10
                    $this->doDetach($relatedDocument, $visited);
2104
                }
2105 10
            } elseif ($relatedDocuments !== null) {
2106 10
                $this->doDetach($relatedDocuments, $visited);
2107
            }
2108
        }
2109 16
    }
2110
    /**
2111
     * Cascades a merge operation to associated documents.
2112
     *
2113
     * @param object $document
2114
     * @param object $managedCopy
2115
     * @param array $visited
2116
     */
2117 12
    private function cascadeMerge($document, $managedCopy, array &$visited)
2118
    {
2119 12
        $class = $this->dm->getClassMetadata(get_class($document));
2120
2121 12
        $associationMappings = array_filter(
2122 12
            $class->associationMappings,
2123
            function ($assoc) { return $assoc['isCascadeMerge']; }
2124
        );
2125
2126 12
        foreach ($associationMappings as $assoc) {
2127 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2128
2129 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2130 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2131
                    // Collections are the same, so there is nothing to do
2132 1
                    continue;
2133
                }
2134
2135 8
                foreach ($relatedDocuments as $relatedDocument) {
2136 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2137
                }
2138 6
            } elseif ($relatedDocuments !== null) {
2139 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2140
            }
2141
        }
2142 12
    }
2143
2144
    /**
2145
     * Cascades the save operation to associated documents.
2146
     *
2147
     * @param object $document
2148
     * @param array $visited
2149
     */
2150 602
    private function cascadePersist($document, array &$visited)
2151
    {
2152 602
        $class = $this->dm->getClassMetadata(get_class($document));
2153
2154 602
        $associationMappings = array_filter(
2155 602
            $class->associationMappings,
2156
            function ($assoc) { return $assoc['isCascadePersist']; }
2157
        );
2158
2159 602
        foreach ($associationMappings as $fieldName => $mapping) {
2160 416
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2161
2162 416
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2163 343
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2164 17
                    if ($relatedDocuments->getOwner() !== $document) {
2165 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2166
                    }
2167
                    // Unwrap so that foreach() does not initialize
2168 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2169
                }
2170
2171 343
                $count = 0;
2172 343
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2173 173
                    if ( ! empty($mapping['embedded'])) {
2174 102
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2175 102
                        if ($knownParent && $knownParent !== $document) {
2176 1
                            $relatedDocument = clone $relatedDocument;
2177 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2178
                        }
2179 102
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2180 102
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2181
                    }
2182 343
                    $this->doPersist($relatedDocument, $visited);
2183
                }
2184 331
            } elseif ($relatedDocuments !== null) {
2185 130
                if ( ! empty($mapping['embedded'])) {
2186 67
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2187 67
                    if ($knownParent && $knownParent !== $document) {
2188 3
                        $relatedDocuments = clone $relatedDocuments;
2189 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2190
                    }
2191 67
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2192
                }
2193 416
                $this->doPersist($relatedDocuments, $visited);
2194
            }
2195
        }
2196 600
    }
2197
2198
    /**
2199
     * Cascades the delete operation to associated documents.
2200
     *
2201
     * @param object $document
2202
     * @param array $visited
2203
     */
2204 69 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...
2205
    {
2206 69
        $class = $this->dm->getClassMetadata(get_class($document));
2207 69
        foreach ($class->fieldMappings as $mapping) {
2208 69
            if ( ! $mapping['isCascadeRemove']) {
2209 68
                continue;
2210
            }
2211 33
            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...
2212 2
                $document->__load();
2213
            }
2214
2215 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2216 33
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2217
                // If its a PersistentCollection initialization is intended! No unwrap!
2218 22
                foreach ($relatedDocuments as $relatedDocument) {
2219 22
                    $this->doRemove($relatedDocument, $visited);
2220
                }
2221 22
            } elseif ($relatedDocuments !== null) {
2222 33
                $this->doRemove($relatedDocuments, $visited);
2223
            }
2224
        }
2225 69
    }
2226
2227
    /**
2228
     * Acquire a lock on the given document.
2229
     *
2230
     * @param object $document
2231
     * @param int $lockMode
2232
     * @param int $lockVersion
2233
     * @throws LockException
2234
     * @throws \InvalidArgumentException
2235
     */
2236 8
    public function lock($document, $lockMode, $lockVersion = null)
2237
    {
2238 8
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2239 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2240
        }
2241
2242 7
        $documentName = get_class($document);
2243 7
        $class = $this->dm->getClassMetadata($documentName);
2244
2245 7
        if ($lockMode == LockMode::OPTIMISTIC) {
2246 2
            if ( ! $class->isVersioned) {
2247 1
                throw LockException::notVersioned($documentName);
2248
            }
2249
2250 1
            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...
2251 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2252 1
                if ($documentVersion != $lockVersion) {
2253 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2254
                }
2255
            }
2256 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2257 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2258
        }
2259 5
    }
2260
2261
    /**
2262
     * Releases a lock on the given document.
2263
     *
2264
     * @param object $document
2265
     * @throws \InvalidArgumentException
2266
     */
2267 1
    public function unlock($document)
2268
    {
2269 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2270
            throw new \InvalidArgumentException("Document is not MANAGED.");
2271
        }
2272 1
        $documentName = get_class($document);
2273 1
        $this->getDocumentPersister($documentName)->unlock($document);
2274 1
    }
2275
2276
    /**
2277
     * Clears the UnitOfWork.
2278
     *
2279
     * @param string|null $documentName if given, only documents of this type will get detached.
2280
     */
2281 376
    public function clear($documentName = null)
2282
    {
2283 376
        if ($documentName === null) {
2284 368
            $this->identityMap =
2285 368
            $this->documentIdentifiers =
2286 368
            $this->originalDocumentData =
2287 368
            $this->documentChangeSets =
2288 368
            $this->documentStates =
2289 368
            $this->scheduledForDirtyCheck =
2290 368
            $this->documentInsertions =
2291 368
            $this->documentUpserts =
2292 368
            $this->documentUpdates =
2293 368
            $this->documentDeletions =
2294 368
            $this->collectionUpdates =
2295 368
            $this->collectionDeletions =
2296 368
            $this->parentAssociations =
2297 368
            $this->embeddedDocumentsRegistry =
2298 368
            $this->orphanRemovals =
2299 368
            $this->hasScheduledCollections = array();
2300
        } else {
2301 8
            $visited = array();
2302 8
            foreach ($this->identityMap as $className => $documents) {
2303 8
                if ($className === $documentName) {
2304 5
                    foreach ($documents as $document) {
2305 8
                        $this->doDetach($document, $visited);
2306
                    }
2307
                }
2308
            }
2309
        }
2310
2311 376 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...
2312
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2313
        }
2314 376
    }
2315
2316
    /**
2317
     * INTERNAL:
2318
     * Schedules an embedded document for removal. The remove() operation will be
2319
     * invoked on that document at the beginning of the next commit of this
2320
     * UnitOfWork.
2321
     *
2322
     * @ignore
2323
     * @param object $document
2324
     */
2325 47
    public function scheduleOrphanRemoval($document)
2326
    {
2327 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2328 47
    }
2329
2330
    /**
2331
     * INTERNAL:
2332
     * Unschedules an embedded or referenced object for removal.
2333
     *
2334
     * @ignore
2335
     * @param object $document
2336
     */
2337 99
    public function unscheduleOrphanRemoval($document)
2338
    {
2339 99
        $oid = spl_object_hash($document);
2340 99
        if (isset($this->orphanRemovals[$oid])) {
2341 1
            unset($this->orphanRemovals[$oid]);
2342
        }
2343 99
    }
2344
2345
    /**
2346
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2347
     *  1) sets owner if it was cloned
2348
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2349
     *  3) NOP if state is OK
2350
     * Returned collection should be used from now on (only important with 2nd point)
2351
     *
2352
     * @param PersistentCollectionInterface $coll
2353
     * @param object $document
2354
     * @param ClassMetadata $class
2355
     * @param string $propName
2356
     * @return PersistentCollectionInterface
2357
     */
2358 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2359
    {
2360 8
        $owner = $coll->getOwner();
2361 8
        if ($owner === null) { // cloned
2362 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2363 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2364 2
            if ( ! $coll->isInitialized()) {
2365 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2366
            }
2367 2
            $newValue = clone $coll;
2368 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2369 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2370 2
            if ($this->isScheduledForUpdate($document)) {
2371
                // @todo following line should be superfluous once collections are stored in change sets
2372
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2373
            }
2374 2
            return $newValue;
2375
        }
2376 6
        return $coll;
2377
    }
2378
2379
    /**
2380
     * INTERNAL:
2381
     * Schedules a complete collection for removal when this UnitOfWork commits.
2382
     *
2383
     * @param PersistentCollectionInterface $coll
2384
     */
2385 35
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2386
    {
2387 35
        $oid = spl_object_hash($coll);
2388 35
        unset($this->collectionUpdates[$oid]);
2389 35
        if ( ! isset($this->collectionDeletions[$oid])) {
2390 35
            $this->collectionDeletions[$oid] = $coll;
2391 35
            $this->scheduleCollectionOwner($coll);
2392
        }
2393 35
    }
2394
2395
    /**
2396
     * Checks whether a PersistentCollection is scheduled for deletion.
2397
     *
2398
     * @param PersistentCollectionInterface $coll
2399
     * @return boolean
2400
     */
2401 194
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2402
    {
2403 194
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2404
    }
2405
2406
    /**
2407
     * INTERNAL:
2408
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2409
     *
2410
     * @param PersistentCollectionInterface $coll
2411
     */
2412 201 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...
2413
    {
2414 201
        $oid = spl_object_hash($coll);
2415 201
        if (isset($this->collectionDeletions[$oid])) {
2416 5
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2417 5
            unset($this->collectionDeletions[$oid]);
2418 5
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2419
        }
2420 201
    }
2421
2422
    /**
2423
     * INTERNAL:
2424
     * Schedules a collection for update when this UnitOfWork commits.
2425
     *
2426
     * @param PersistentCollectionInterface $coll
2427
     */
2428 222
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2429
    {
2430 222
        $mapping = $coll->getMapping();
2431 222
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2432
            /* There is no need to $unset collection if it will be $set later
2433
             * This is NOP if collection is not scheduled for deletion
2434
             */
2435 23
            $this->unscheduleCollectionDeletion($coll);
2436
        }
2437 222
        $oid = spl_object_hash($coll);
2438 222
        if ( ! isset($this->collectionUpdates[$oid])) {
2439 222
            $this->collectionUpdates[$oid] = $coll;
2440 222
            $this->scheduleCollectionOwner($coll);
2441
        }
2442 222
    }
2443
2444
    /**
2445
     * INTERNAL:
2446
     * Unschedules a collection from being updated when this UnitOfWork commits.
2447
     *
2448
     * @param PersistentCollectionInterface $coll
2449
     */
2450 201 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...
2451
    {
2452 201
        $oid = spl_object_hash($coll);
2453 201
        if (isset($this->collectionUpdates[$oid])) {
2454 191
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2455 191
            unset($this->collectionUpdates[$oid]);
2456 191
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2457
        }
2458 201
    }
2459
2460
    /**
2461
     * Checks whether a PersistentCollection is scheduled for update.
2462
     *
2463
     * @param PersistentCollectionInterface $coll
2464
     * @return boolean
2465
     */
2466 112
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2467
    {
2468 112
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2469
    }
2470
2471
    /**
2472
     * INTERNAL:
2473
     * Gets PersistentCollections that have been visited during computing change
2474
     * set of $document
2475
     *
2476
     * @param object $document
2477
     * @return PersistentCollectionInterface[]
2478
     */
2479 556
    public function getVisitedCollections($document)
2480
    {
2481 556
        $oid = spl_object_hash($document);
2482 556
        return isset($this->visitedCollections[$oid])
2483 225
                ? $this->visitedCollections[$oid]
2484 556
                : array();
2485
    }
2486
2487
    /**
2488
     * INTERNAL:
2489
     * Gets PersistentCollections that are scheduled to update and related to $document
2490
     *
2491
     * @param object $document
2492
     * @return array
2493
     */
2494 556
    public function getScheduledCollections($document)
2495
    {
2496 556
        $oid = spl_object_hash($document);
2497 556
        return isset($this->hasScheduledCollections[$oid])
2498 223
                ? $this->hasScheduledCollections[$oid]
2499 556
                : array();
2500
    }
2501
2502
    /**
2503
     * Checks whether the document is related to a PersistentCollection
2504
     * scheduled for update or deletion.
2505
     *
2506
     * @param object $document
2507
     * @return boolean
2508
     */
2509 43
    public function hasScheduledCollections($document)
2510
    {
2511 43
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2512
    }
2513
2514
    /**
2515
     * Marks the PersistentCollection's top-level owner as having a relation to
2516
     * a collection scheduled for update or deletion.
2517
     *
2518
     * If the owner is not scheduled for any lifecycle action, it will be
2519
     * scheduled for update to ensure that versioning takes place if necessary.
2520
     *
2521
     * If the collection is nested within atomic collection, it is immediately
2522
     * unscheduled and atomic one is scheduled for update instead. This makes
2523
     * calculating update data way easier.
2524
     *
2525
     * @param PersistentCollectionInterface $coll
2526
     */
2527 224
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2528
    {
2529 224
        $document = $this->getOwningDocument($coll->getOwner());
2530 224
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2531
2532 224
        if ($document !== $coll->getOwner()) {
2533 19
            $parent = $coll->getOwner();
2534 19
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2535 19
                list($mapping, $parent, ) = $parentAssoc;
2536
            }
2537 19
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2538 3
                $class = $this->dm->getClassMetadata(get_class($document));
2539 3
                $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...
2540 3
                $this->scheduleCollectionUpdate($atomicCollection);
2541 3
                $this->unscheduleCollectionDeletion($coll);
2542 3
                $this->unscheduleCollectionUpdate($coll);
2543
            }
2544
        }
2545
2546 224
        if ( ! $this->isDocumentScheduled($document)) {
2547 39
            $this->scheduleForUpdate($document);
2548
        }
2549 224
    }
2550
2551
    /**
2552
     * Get the top-most owning document of a given document
2553
     *
2554
     * If a top-level document is provided, that same document will be returned.
2555
     * For an embedded document, we will walk through parent associations until
2556
     * we find a top-level document.
2557
     *
2558
     * @param object $document
2559
     * @throws \UnexpectedValueException when a top-level document could not be found
2560
     * @return object
2561
     */
2562 226
    public function getOwningDocument($document)
2563
    {
2564 226
        $class = $this->dm->getClassMetadata(get_class($document));
2565 226
        while ($class->isEmbeddedDocument) {
2566 33
            $parentAssociation = $this->getParentAssociation($document);
2567
2568 33
            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...
2569
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2570
            }
2571
2572 33
            list(, $document, ) = $parentAssociation;
2573 33
            $class = $this->dm->getClassMetadata(get_class($document));
2574
        }
2575
2576 226
        return $document;
2577
    }
2578
2579
    /**
2580
     * Gets the class name for an association (embed or reference) with respect
2581
     * to any discriminator value.
2582
     *
2583
     * @param array      $mapping Field mapping for the association
2584
     * @param array|null $data    Data for the embedded document or reference
2585
     * @return string Class name.
2586
     */
2587 217
    public function getClassNameForAssociation(array $mapping, $data)
2588
    {
2589 217
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2590
2591 217
        $discriminatorValue = null;
2592 217
        if (isset($discriminatorField, $data[$discriminatorField])) {
2593 21
            $discriminatorValue = $data[$discriminatorField];
2594 197
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2595
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2596
        }
2597
2598 217
        if ($discriminatorValue !== null) {
2599 21
            return $mapping['discriminatorMap'][$discriminatorValue]
2600 21
                ?? $discriminatorValue;
2601
        }
2602
2603 197
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2604
2605 197 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...
2606 15
            $discriminatorValue = $data[$class->discriminatorField];
2607 183
        } elseif ($class->defaultDiscriminatorValue !== null) {
2608 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2609
        }
2610
2611 197
        if ($discriminatorValue !== null) {
2612 16
            return isset($class->discriminatorMap[$discriminatorValue])
2613 14
                ? $class->discriminatorMap[$discriminatorValue]
2614 16
                : $discriminatorValue;
2615
        }
2616
2617 182
        return $mapping['targetDocument'];
2618
    }
2619
2620
    /**
2621
     * INTERNAL:
2622
     * Creates a document. Used for reconstitution of documents during hydration.
2623
     *
2624
     * @ignore
2625
     * @param string $className The name of the document class.
2626
     * @param array $data The data for the document.
2627
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2628
     * @param object $document The document to be hydrated into in case of creation
2629
     * @return object The document instance.
2630
     * @internal Highly performance-sensitive method.
2631
     */
2632 384
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2633
    {
2634 384
        $class = $this->dm->getClassMetadata($className);
2635
2636
        // @TODO figure out how to remove this
2637 384
        $discriminatorValue = null;
2638 384 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...
2639 19
            $discriminatorValue = $data[$class->discriminatorField];
2640 376
        } elseif (isset($class->defaultDiscriminatorValue)) {
2641 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2642
        }
2643
2644 384
        if ($discriminatorValue !== null) {
2645 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2646 18
                ? $class->discriminatorMap[$discriminatorValue]
2647 20
                : $discriminatorValue;
2648
2649 20
            $class = $this->dm->getClassMetadata($className);
2650
2651 20
            unset($data[$class->discriminatorField]);
2652
        }
2653
        
2654 384
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2655 2
            $document = $class->newInstance();
2656 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2657 2
            return $document;
2658
        }
2659
2660 383
        $isManagedObject = false;
2661 383
        if (! $class->isQueryResultDocument) {
2662 380
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2663 380
            $serializedId = serialize($id);
2664 380
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2665
        }
2666
2667 383
        if ($isManagedObject) {
2668 104
            $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...
2669 104
            $oid = spl_object_hash($document);
2670 104
            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...
2671 14
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2672 14
                $overrideLocalValues = true;
2673 14
                if ($document instanceof NotifyPropertyChanged) {
2674 14
                    $document->addPropertyChangedListener($this);
2675
                }
2676
            } else {
2677 96
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2678
            }
2679 104
            if ($overrideLocalValues) {
2680 53
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2681 104
                $this->originalDocumentData[$oid] = $data;
2682
            }
2683
        } else {
2684 344
            if ($document === null) {
2685 344
                $document = $class->newInstance();
2686
            }
2687
2688 344
            if (! $class->isQueryResultDocument) {
2689 340
                $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...
2690 340
                $oid = spl_object_hash($document);
2691 340
                $this->documentStates[$oid] = self::STATE_MANAGED;
2692 340
                $this->identityMap[$class->name][$serializedId] = $document;
2693
            }
2694
2695 344
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2696
2697 344
            if (! $class->isQueryResultDocument) {
2698 340
                $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...
2699
            }
2700
        }
2701
2702 383
        return $document;
2703
    }
2704
2705
    /**
2706
     * Initializes (loads) an uninitialized persistent collection of a document.
2707
     *
2708
     * @param PersistentCollectionInterface $collection The collection to initialize.
2709
     */
2710 162
    public function loadCollection(PersistentCollectionInterface $collection)
2711
    {
2712 162
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2713 162
        $this->lifecycleEventManager->postCollectionLoad($collection);
2714 162
    }
2715
2716
    /**
2717
     * Gets the identity map of the UnitOfWork.
2718
     *
2719
     * @return array
2720
     */
2721
    public function getIdentityMap()
2722
    {
2723
        return $this->identityMap;
2724
    }
2725
2726
    /**
2727
     * Gets the original data of a document. The original data is the data that was
2728
     * present at the time the document was reconstituted from the database.
2729
     *
2730
     * @param object $document
2731
     * @return array
2732
     */
2733 1
    public function getOriginalDocumentData($document)
2734
    {
2735 1
        $oid = spl_object_hash($document);
2736 1
        if (isset($this->originalDocumentData[$oid])) {
2737 1
            return $this->originalDocumentData[$oid];
2738
        }
2739
        return array();
2740
    }
2741
2742
    /**
2743
     * @ignore
2744
     */
2745 58
    public function setOriginalDocumentData($document, array $data)
2746
    {
2747 58
        $oid = spl_object_hash($document);
2748 58
        $this->originalDocumentData[$oid] = $data;
2749 58
        unset($this->documentChangeSets[$oid]);
2750 58
    }
2751
2752
    /**
2753
     * INTERNAL:
2754
     * Sets a property value of the original data array of a document.
2755
     *
2756
     * @ignore
2757
     * @param string $oid
2758
     * @param string $property
2759
     * @param mixed $value
2760
     */
2761 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2762
    {
2763 3
        $this->originalDocumentData[$oid][$property] = $value;
2764 3
    }
2765
2766
    /**
2767
     * Gets the identifier of a document.
2768
     *
2769
     * @param object $document
2770
     * @return mixed The identifier value
2771
     */
2772 417
    public function getDocumentIdentifier($document)
2773
    {
2774 417
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2775 417
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2776
    }
2777
2778
    /**
2779
     * Checks whether the UnitOfWork has any pending insertions.
2780
     *
2781
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2782
     */
2783
    public function hasPendingInsertions()
2784
    {
2785
        return ! empty($this->documentInsertions);
2786
    }
2787
2788
    /**
2789
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2790
     * number of documents in the identity map.
2791
     *
2792
     * @return integer
2793
     */
2794 2
    public function size()
2795
    {
2796 2
        $count = 0;
2797 2
        foreach ($this->identityMap as $documentSet) {
2798 2
            $count += count($documentSet);
2799
        }
2800 2
        return $count;
2801
    }
2802
2803
    /**
2804
     * INTERNAL:
2805
     * Registers a document as managed.
2806
     *
2807
     * TODO: This method assumes that $id is a valid PHP identifier for the
2808
     * document class. If the class expects its database identifier to be an
2809
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2810
     * document identifiers map will become inconsistent with the identity map.
2811
     * In the future, we may want to round-trip $id through a PHP and database
2812
     * conversion and throw an exception if it's inconsistent.
2813
     *
2814
     * @param object $document The document.
2815
     * @param array $id The identifier values.
2816
     * @param array $data The original document data.
2817
     */
2818 366
    public function registerManaged($document, $id, $data)
2819
    {
2820 366
        $oid = spl_object_hash($document);
2821 366
        $class = $this->dm->getClassMetadata(get_class($document));
2822
2823 366
        if ( ! $class->identifier || $id === null) {
2824 91
            $this->documentIdentifiers[$oid] = $oid;
2825
        } else {
2826 360
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2827
        }
2828
2829 366
        $this->documentStates[$oid] = self::STATE_MANAGED;
2830 366
        $this->originalDocumentData[$oid] = $data;
2831 366
        $this->addToIdentityMap($document);
2832 366
    }
2833
2834
    /**
2835
     * INTERNAL:
2836
     * Clears the property changeset of the document with the given OID.
2837
     *
2838
     * @param string $oid The document's OID.
2839
     */
2840
    public function clearDocumentChangeSet($oid)
2841
    {
2842
        $this->documentChangeSets[$oid] = array();
2843
    }
2844
2845
    /* PropertyChangedListener implementation */
2846
2847
    /**
2848
     * Notifies this UnitOfWork of a property change in a document.
2849
     *
2850
     * @param object $document The document that owns the property.
2851
     * @param string $propertyName The name of the property that changed.
2852
     * @param mixed $oldValue The old value of the property.
2853
     * @param mixed $newValue The new value of the property.
2854
     */
2855 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2856
    {
2857 2
        $oid = spl_object_hash($document);
2858 2
        $class = $this->dm->getClassMetadata(get_class($document));
2859
2860 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2861 1
            return; // ignore non-persistent fields
2862
        }
2863
2864
        // Update changeset and mark document for synchronization
2865 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2866 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2867 2
            $this->scheduleForDirtyCheck($document);
2868
        }
2869 2
    }
2870
2871
    /**
2872
     * Gets the currently scheduled document insertions in this UnitOfWork.
2873
     *
2874
     * @return array
2875
     */
2876 2
    public function getScheduledDocumentInsertions()
2877
    {
2878 2
        return $this->documentInsertions;
2879
    }
2880
2881
    /**
2882
     * Gets the currently scheduled document upserts in this UnitOfWork.
2883
     *
2884
     * @return array
2885
     */
2886 1
    public function getScheduledDocumentUpserts()
2887
    {
2888 1
        return $this->documentUpserts;
2889
    }
2890
2891
    /**
2892
     * Gets the currently scheduled document updates in this UnitOfWork.
2893
     *
2894
     * @return array
2895
     */
2896
    public function getScheduledDocumentUpdates()
2897
    {
2898
        return $this->documentUpdates;
2899
    }
2900
2901
    /**
2902
     * Gets the currently scheduled document deletions in this UnitOfWork.
2903
     *
2904
     * @return array
2905
     */
2906
    public function getScheduledDocumentDeletions()
2907
    {
2908
        return $this->documentDeletions;
2909
    }
2910
2911
    /**
2912
     * Get the currently scheduled complete collection deletions
2913
     *
2914
     * @return array
2915
     */
2916
    public function getScheduledCollectionDeletions()
2917
    {
2918
        return $this->collectionDeletions;
2919
    }
2920
2921
    /**
2922
     * Gets the currently scheduled collection inserts, updates and deletes.
2923
     *
2924
     * @return array
2925
     */
2926
    public function getScheduledCollectionUpdates()
2927
    {
2928
        return $this->collectionUpdates;
2929
    }
2930
2931
    /**
2932
     * Helper method to initialize a lazy loading proxy or persistent collection.
2933
     *
2934
     * @param object
2935
     * @return void
2936
     */
2937
    public function initializeObject($obj)
2938
    {
2939
        if ($obj instanceof Proxy) {
2940
            $obj->__load();
2941
        } elseif ($obj instanceof PersistentCollectionInterface) {
2942
            $obj->initialize();
2943
        }
2944
    }
2945
2946 1
    private function objToStr($obj)
2947
    {
2948 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2949
    }
2950
}
2951