Completed
Pull Request — master (#1612)
by Maciej
10:31
created

UnitOfWork::executeUpdates()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5.0144

Importance

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

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

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

Loading history...
771 6
                    continue;
772
                }
773
774
                // Persistent collection was exchanged with the "originally"
775
                // created one. This can only mean it was cloned and replaced
776
                // on another document.
777 239
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
778 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
779
                }
780
781
                // if embed-many or reference-many relationship
782 239
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
783 119
                    $changeSet[$propName] = array($orgValue, $actualValue);
784
                    /* If original collection was exchanged with a non-empty value
785
                     * and $set will be issued, there is no need to $unset it first
786
                     */
787 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
788 28
                        continue;
789
                    }
790 99
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
791 18
                        $this->scheduleCollectionDeletion($orgValue);
792
                    }
793 99
                    continue;
794
                }
795
796
                // skip equivalent date values
797 157
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
798 37
                    $dateType = Type::getType('date');
799 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
800 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
801
802 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
803 30
                        continue;
804
                    }
805
                }
806
807
                // regular field
808 140
                $changeSet[$propName] = array($orgValue, $actualValue);
809
            }
810 299
            if ($changeSet) {
811 245
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
812 21
                    ? $changeSet + $this->documentChangeSets[$oid]
813 240
                    : $changeSet;
814
815 245
                $this->originalDocumentData[$oid] = $actualData;
816 245
                $this->scheduleForUpdate($document);
817
            }
818
        }
819
820
        // Look for changes in associations of the document
821 636
        $associationMappings = array_filter(
822 636
            $class->associationMappings,
823
            function ($assoc) { return empty($assoc['notSaved']); }
824
        );
825
826 636
        foreach ($associationMappings as $mapping) {
827 480
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
828
829 480
            if ($value === null) {
830 325
                continue;
831
            }
832
833 467
            $this->computeAssociationChanges($document, $mapping, $value);
834
835 466
            if (isset($mapping['reference'])) {
836 353
                continue;
837
            }
838
839 363
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
840
841 363
            foreach ($values as $obj) {
842 187
                $oid2 = spl_object_hash($obj);
843
844 187
                if (isset($this->documentChangeSets[$oid2])) {
845 185
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
846
                        // instance of $value is the same as it was previously otherwise there would be
847
                        // change set already in place
848 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
849
                    }
850
851 185
                    if ( ! $isNewDocument) {
852 80
                        $this->scheduleForUpdate($document);
853
                    }
854
855 363
                    break;
856
                }
857
            }
858
        }
859 635
    }
860
861
    /**
862
     * Computes all the changes that have been done to documents and collections
863
     * since the last commit and stores these changes in the _documentChangeSet map
864
     * temporarily for access by the persisters, until the UoW commit is finished.
865
     */
866 634
    public function computeChangeSets()
867
    {
868 634
        $this->computeScheduleInsertsChangeSets();
869 633
        $this->computeScheduleUpsertsChangeSets();
870
871
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
872 633
        foreach ($this->identityMap as $className => $documents) {
873 633
            $class = $this->dm->getClassMetadata($className);
874 633
            if ($class->isEmbeddedDocument) {
875
                /* we do not want to compute changes to embedded documents up front
876
                 * in case embedded document was replaced and its changeset
877
                 * would corrupt data. Embedded documents' change set will
878
                 * be calculated by reachability from owning document.
879
                 */
880 176
                continue;
881
            }
882
883
            // If change tracking is explicit or happens through notification, then only compute
884
            // changes on document of that type that are explicitly marked for synchronization.
885
            switch (true) {
886 633
                case ($class->isChangeTrackingDeferredImplicit()):
887 632
                    $documentsToProcess = $documents;
888 632
                    break;
889
890 4
                case (isset($this->scheduledForDirtyCheck[$className])):
891 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
892 3
                    break;
893
894
                default:
895 4
                    $documentsToProcess = array();
896
897
            }
898
899 633
            foreach ($documentsToProcess as $document) {
900
                // Ignore uninitialized proxy objects
901 629
                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...
902 10
                    continue;
903
                }
904
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
905 629
                $oid = spl_object_hash($document);
906 629 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...
907 629
                    && ! isset($this->documentUpserts[$oid])
908 629
                    && ! isset($this->documentDeletions[$oid])
909 629
                    && isset($this->documentStates[$oid])
910
                ) {
911 633
                    $this->computeChangeSet($class, $document);
912
                }
913
            }
914
        }
915 633
    }
916
917
    /**
918
     * Computes the changes of an association.
919
     *
920
     * @param object $parentDocument
921
     * @param array $assoc
922
     * @param mixed $value The value of the association.
923
     * @throws \InvalidArgumentException
924
     */
925 467
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
926
    {
927 467
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
928 467
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
929 467
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
930
931 467
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
932 8
            return;
933
        }
934
935 466
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
936 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
937 254
                $this->scheduleCollectionUpdate($value);
938
            }
939 258
            $topmostOwner = $this->getOwningDocument($value->getOwner());
940 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
941 258
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
942 147
                $value->initialize();
943 147
                foreach ($value->getDeletedDocuments() as $orphan) {
944 23
                    $this->scheduleOrphanRemoval($orphan);
945
                }
946
            }
947
        }
948
949
        // Look through the documents, and in any of their associations,
950
        // for transient (new) documents, recursively. ("Persistence by reachability")
951
        // Unwrap. Uninitialized collections will simply be empty.
952 466
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
953
954 466
        $count = 0;
955 466
        foreach ($unwrappedValue as $key => $entry) {
956 371
            if ( ! is_object($entry)) {
957 1
                throw new \InvalidArgumentException(
958 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
959
                );
960
            }
961
962 370
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
963
964 370
            $state = $this->getDocumentState($entry, self::STATE_NEW);
965
966
            // Handle "set" strategy for multi-level hierarchy
967 370
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
968 370
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
969
970 370
            $count++;
971
972
            switch ($state) {
973 370
                case self::STATE_NEW:
974 68
                    if ( ! $assoc['isCascadePersist']) {
975
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
976
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
977
                            . ' Explicitly persist the new document or configure cascading persist operations'
978
                            . ' on the relationship.');
979
                    }
980
981 68
                    $this->persistNew($targetClass, $entry);
982 68
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
983 68
                    $this->computeChangeSet($targetClass, $entry);
984 68
                    break;
985
986 365
                case self::STATE_MANAGED:
987 365
                    if ($targetClass->isEmbeddedDocument) {
988 178
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
989 178
                        if ($knownParent && $knownParent !== $parentDocument) {
990 9
                            $entry = clone $entry;
991 9
                            if ($assoc['type'] === ClassMetadata::ONE) {
992 6
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
993 6
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
994 6
                                $poid = spl_object_hash($parentDocument);
995 6
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
996 6
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
997
                                }
998
                            } else {
999
                                // must use unwrapped value to not trigger orphan removal
1000 7
                                $unwrappedValue[$key] = $entry;
1001
                            }
1002 9
                            $this->persistNew($targetClass, $entry);
1003
                        }
1004 178
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
1005 178
                        $this->computeChangeSet($targetClass, $entry);
1006
                    }
1007 365
                    break;
1008
1009 1
                case self::STATE_REMOVED:
1010
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1011
                    // and remove the element from Collection.
1012 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1013
                        unset($value[$key]);
1014
                    }
1015 1
                    break;
1016
1017
                case self::STATE_DETACHED:
1018
                    // Can actually not happen right now as we assume STATE_NEW,
1019
                    // so the exception will be raised from the DBAL layer (constraint violation).
1020
                    throw new \InvalidArgumentException('A detached document was found through a '
1021
                        . 'relationship during cascading a persist operation.');
1022
1023 370
                default:
1024
                    // MANAGED associated documents are already taken into account
1025
                    // during changeset calculation anyway, since they are in the identity map.
1026
1027
            }
1028
        }
1029 465
    }
1030
1031
    /**
1032
     * INTERNAL:
1033
     * Computes the changeset of an individual document, independently of the
1034
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1035
     *
1036
     * The passed document must be a managed document. If the document already has a change set
1037
     * because this method is invoked during a commit cycle then the change sets are added.
1038
     * whereby changes detected in this method prevail.
1039
     *
1040
     * @ignore
1041
     * @param ClassMetadata $class The class descriptor of the document.
1042
     * @param object $document The document for which to (re)calculate the change set.
1043
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1044
     */
1045 21
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1046
    {
1047
        // Ignore uninitialized proxy objects
1048 21
        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...
1049 1
            return;
1050
        }
1051
1052 20
        $oid = spl_object_hash($document);
1053
1054 20
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1055
            throw new \InvalidArgumentException('Document must be managed.');
1056
        }
1057
1058 20
        if ( ! $class->isInheritanceTypeNone()) {
1059 2
            $class = $this->dm->getClassMetadata(get_class($document));
1060
        }
1061
1062 20
        $this->computeOrRecomputeChangeSet($class, $document, true);
1063 20
    }
1064
1065
    /**
1066
     * @param ClassMetadata $class
1067
     * @param object $document
1068
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1069
     */
1070 669
    private function persistNew(ClassMetadata $class, $document)
1071
    {
1072 669
        $this->lifecycleEventManager->prePersist($class, $document);
1073 669
        $oid = spl_object_hash($document);
1074 669
        $upsert = false;
1075 669
        if ($class->identifier) {
1076 669
            $idValue = $class->getIdentifierValue($document);
1077 669
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1078
1079 669
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1080 3
                throw new \InvalidArgumentException(sprintf(
1081 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1082 3
                    get_class($document)
1083
                ));
1084
            }
1085
1086 668
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1087 1
                throw new \InvalidArgumentException(sprintf(
1088 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1089 1
                    get_class($document)
1090
                ));
1091
            }
1092
1093 667
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1094 583
                $idValue = $class->idGenerator->generate($this->dm, $document);
1095 583
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1096 583
                $class->setIdentifierValue($document, $idValue);
1097
            }
1098
1099 667
            $this->documentIdentifiers[$oid] = $idValue;
1100
        } else {
1101
            // this is for embedded documents without identifiers
1102 161
            $this->documentIdentifiers[$oid] = $oid;
1103
        }
1104
1105 667
        $this->documentStates[$oid] = self::STATE_MANAGED;
1106
1107 667
        if ($upsert) {
1108 94
            $this->scheduleForUpsert($class, $document);
1109
        } else {
1110 592
            $this->scheduleForInsert($class, $document);
1111
        }
1112 667
    }
1113
1114
    /**
1115
     * Executes all document insertions for documents of the specified type.
1116
     *
1117
     * @param ClassMetadata $class
1118
     * @param array $documents Array of documents to insert
1119
     * @param array $options Array of options to be used with batchInsert()
1120
     */
1121 551 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...
1122
    {
1123 551
        $persister = $this->getDocumentPersister($class->name);
1124
1125 551
        foreach ($documents as $oid => $document) {
1126 551
            $persister->addInsert($document);
1127 551
            unset($this->documentInsertions[$oid]);
1128
        }
1129
1130 551
        $persister->executeInserts($options);
1131
1132 550
        foreach ($documents as $document) {
1133 550
            $this->lifecycleEventManager->postPersist($class, $document);
1134
        }
1135 550
    }
1136
1137
    /**
1138
     * Executes all document upserts for documents of the specified type.
1139
     *
1140
     * @param ClassMetadata $class
1141
     * @param array $documents Array of documents to upsert
1142
     * @param array $options Array of options to be used with batchInsert()
1143
     */
1144 90 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...
1145
    {
1146 90
        $persister = $this->getDocumentPersister($class->name);
1147
1148
1149 90
        foreach ($documents as $oid => $document) {
1150 90
            $persister->addUpsert($document);
1151 90
            unset($this->documentUpserts[$oid]);
1152
        }
1153
1154 90
        $persister->executeUpserts($options);
1155
1156 90
        foreach ($documents as $document) {
1157 90
            $this->lifecycleEventManager->postPersist($class, $document);
1158
        }
1159 90
    }
1160
1161
    /**
1162
     * Executes all document updates for documents of the specified type.
1163
     *
1164
     * @param Mapping\ClassMetadata $class
1165
     * @param array $documents Array of documents to update
1166
     * @param array $options Array of options to be used with update()
1167
     */
1168 237
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1169
    {
1170 237
        if ($class->isReadOnly) {
1171
            return;
1172
        }
1173
1174 237
        $className = $class->name;
1175 237
        $persister = $this->getDocumentPersister($className);
1176
1177 237
        foreach ($documents as $oid => $document) {
1178 237
            $this->lifecycleEventManager->preUpdate($class, $document);
1179
1180 237
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1181 235
                $persister->update($document, $options);
1182
            }
1183
1184 230
            unset($this->documentUpdates[$oid]);
1185
1186 230
            $this->lifecycleEventManager->postUpdate($class, $document);
1187
        }
1188 229
    }
1189
1190
    /**
1191
     * Executes all document deletions for documents of the specified type.
1192
     *
1193
     * @param ClassMetadata $class
1194
     * @param array $documents Array of documents to delete
1195
     * @param array $options Array of options to be used with remove()
1196
     */
1197 73
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1198
    {
1199 73
        $persister = $this->getDocumentPersister($class->name);
1200
1201 73
        foreach ($documents as $oid => $document) {
1202 73
            if ( ! $class->isEmbeddedDocument) {
1203 35
                $persister->delete($document, $options);
1204
            }
1205
            unset(
1206 71
                $this->documentDeletions[$oid],
1207 71
                $this->documentIdentifiers[$oid],
1208 71
                $this->originalDocumentData[$oid]
1209
            );
1210
1211
            // Clear snapshot information for any referenced PersistentCollection
1212
            // http://www.doctrine-project.org/jira/browse/MODM-95
1213 71
            foreach ($class->associationMappings as $fieldMapping) {
1214 45
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1215 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1216 27
                    if ($value instanceof PersistentCollectionInterface) {
1217 45
                        $value->clearSnapshot();
1218
                    }
1219
                }
1220
            }
1221
1222
            // Document with this $oid after deletion treated as NEW, even if the $oid
1223
            // is obtained by a new document because the old one went out of scope.
1224 71
            $this->documentStates[$oid] = self::STATE_NEW;
1225
1226 71
            $this->lifecycleEventManager->postRemove($class, $document);
1227
        }
1228 71
    }
1229
1230
    /**
1231
     * Schedules a document for insertion into the database.
1232
     * If the document already has an identifier, it will be added to the
1233
     * identity map.
1234
     *
1235
     * @param ClassMetadata $class
1236
     * @param object $document The document to schedule for insertion.
1237
     * @throws \InvalidArgumentException
1238
     */
1239 595
    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...
1240
    {
1241 595
        $oid = spl_object_hash($document);
1242
1243 595
        if (isset($this->documentUpdates[$oid])) {
1244
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1245
        }
1246 595
        if (isset($this->documentDeletions[$oid])) {
1247
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1248
        }
1249 595
        if (isset($this->documentInsertions[$oid])) {
1250
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1251
        }
1252
1253 595
        $this->documentInsertions[$oid] = $document;
1254
1255 595
        if (isset($this->documentIdentifiers[$oid])) {
1256 592
            $this->addToIdentityMap($document);
1257
        }
1258 595
    }
1259
1260
    /**
1261
     * Schedules a document for upsert into the database and adds it to the
1262
     * identity map
1263
     *
1264
     * @param ClassMetadata $class
1265
     * @param object $document The document to schedule for upsert.
1266
     * @throws \InvalidArgumentException
1267
     */
1268 97
    public function scheduleForUpsert(ClassMetadata $class, $document)
1269
    {
1270 97
        $oid = spl_object_hash($document);
1271
1272 97
        if ($class->isEmbeddedDocument) {
1273
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1274
        }
1275 97
        if (isset($this->documentUpdates[$oid])) {
1276
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1277
        }
1278 97
        if (isset($this->documentDeletions[$oid])) {
1279
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1280
        }
1281 97
        if (isset($this->documentUpserts[$oid])) {
1282
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1283
        }
1284
1285 97
        $this->documentUpserts[$oid] = $document;
1286 97
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1287 97
        $this->addToIdentityMap($document);
1288 97
    }
1289
1290
    /**
1291
     * Checks whether a document is scheduled for insertion.
1292
     *
1293
     * @param object $document
1294
     * @return boolean
1295
     */
1296 108
    public function isScheduledForInsert($document)
1297
    {
1298 108
        return isset($this->documentInsertions[spl_object_hash($document)]);
1299
    }
1300
1301
    /**
1302
     * Checks whether a document is scheduled for upsert.
1303
     *
1304
     * @param object $document
1305
     * @return boolean
1306
     */
1307 5
    public function isScheduledForUpsert($document)
1308
    {
1309 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1310
    }
1311
1312
    /**
1313
     * Schedules a document for being updated.
1314
     *
1315
     * @param object $document The document to schedule for being updated.
1316
     * @throws \InvalidArgumentException
1317
     */
1318 246
    public function scheduleForUpdate($document)
1319
    {
1320 246
        $oid = spl_object_hash($document);
1321 246
        if ( ! isset($this->documentIdentifiers[$oid])) {
1322
            throw new \InvalidArgumentException('Document has no identity.');
1323
        }
1324
1325 246
        if (isset($this->documentDeletions[$oid])) {
1326
            throw new \InvalidArgumentException('Document is removed.');
1327
        }
1328
1329 246
        if ( ! isset($this->documentUpdates[$oid])
1330 246
            && ! isset($this->documentInsertions[$oid])
1331 246
            && ! isset($this->documentUpserts[$oid])) {
1332 242
            $this->documentUpdates[$oid] = $document;
1333
        }
1334 246
    }
1335
1336
    /**
1337
     * Checks whether a document is registered as dirty in the unit of work.
1338
     * Note: Is not very useful currently as dirty documents are only registered
1339
     * at commit time.
1340
     *
1341
     * @param object $document
1342
     * @return boolean
1343
     */
1344 22
    public function isScheduledForUpdate($document)
1345
    {
1346 22
        return isset($this->documentUpdates[spl_object_hash($document)]);
1347
    }
1348
1349 1
    public function isScheduledForDirtyCheck($document)
1350
    {
1351 1
        $class = $this->dm->getClassMetadata(get_class($document));
1352 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1353
    }
1354
1355
    /**
1356
     * INTERNAL:
1357
     * Schedules a document for deletion.
1358
     *
1359
     * @param object $document
1360
     */
1361 78
    public function scheduleForDelete($document)
1362
    {
1363 78
        $oid = spl_object_hash($document);
1364
1365 78
        if (isset($this->documentInsertions[$oid])) {
1366 2
            if ($this->isInIdentityMap($document)) {
1367 2
                $this->removeFromIdentityMap($document);
1368
            }
1369 2
            unset($this->documentInsertions[$oid]);
1370 2
            return; // document has not been persisted yet, so nothing more to do.
1371
        }
1372
1373 77
        if ( ! $this->isInIdentityMap($document)) {
1374 2
            return; // ignore
1375
        }
1376
1377 76
        $this->removeFromIdentityMap($document);
1378 76
        $this->documentStates[$oid] = self::STATE_REMOVED;
1379
1380 76
        if (isset($this->documentUpdates[$oid])) {
1381
            unset($this->documentUpdates[$oid]);
1382
        }
1383 76
        if ( ! isset($this->documentDeletions[$oid])) {
1384 76
            $this->documentDeletions[$oid] = $document;
1385
        }
1386 76
    }
1387
1388
    /**
1389
     * Checks whether a document is registered as removed/deleted with the unit
1390
     * of work.
1391
     *
1392
     * @param object $document
1393
     * @return boolean
1394
     */
1395 8
    public function isScheduledForDelete($document)
1396
    {
1397 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1398
    }
1399
1400
    /**
1401
     * Checks whether a document is scheduled for insertion, update or deletion.
1402
     *
1403
     * @param $document
1404
     * @return boolean
1405
     */
1406 257
    public function isDocumentScheduled($document)
1407
    {
1408 257
        $oid = spl_object_hash($document);
1409 257
        return isset($this->documentInsertions[$oid]) ||
1410 132
            isset($this->documentUpserts[$oid]) ||
1411 122
            isset($this->documentUpdates[$oid]) ||
1412 257
            isset($this->documentDeletions[$oid]);
1413
    }
1414
1415
    /**
1416
     * INTERNAL:
1417
     * Registers a document in the identity map.
1418
     *
1419
     * Note that documents in a hierarchy are registered with the class name of
1420
     * the root document. Identifiers are serialized before being used as array
1421
     * keys to allow differentiation of equal, but not identical, values.
1422
     *
1423
     * @ignore
1424
     * @param object $document  The document to register.
1425
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1426
     *                  the document in question is already managed.
1427
     */
1428 700
    public function addToIdentityMap($document)
1429
    {
1430 700
        $class = $this->dm->getClassMetadata(get_class($document));
1431 700
        $id = $this->getIdForIdentityMap($document);
1432
1433 700
        if (isset($this->identityMap[$class->name][$id])) {
1434 56
            return false;
1435
        }
1436
1437 700
        $this->identityMap[$class->name][$id] = $document;
1438
1439 700
        if ($document instanceof NotifyPropertyChanged &&
1440 700
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1441 4
            $document->addPropertyChangedListener($this);
1442
        }
1443
1444 700
        return true;
1445
    }
1446
1447
    /**
1448
     * Gets the state of a document with regard to the current unit of work.
1449
     *
1450
     * @param object   $document
1451
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1452
     *                         This parameter can be set to improve performance of document state detection
1453
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1454
     *                         is either known or does not matter for the caller of the method.
1455
     * @return int The document state.
1456
     */
1457 674
    public function getDocumentState($document, $assume = null)
1458
    {
1459 674
        $oid = spl_object_hash($document);
1460
1461 674
        if (isset($this->documentStates[$oid])) {
1462 417
            return $this->documentStates[$oid];
1463
        }
1464
1465 672
        $class = $this->dm->getClassMetadata(get_class($document));
1466
1467 672
        if ($class->isEmbeddedDocument) {
1468 196
            return self::STATE_NEW;
1469
        }
1470
1471 669
        if ($assume !== null) {
1472 666
            return $assume;
1473
        }
1474
1475
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1476
         * known. Note that you cannot remember the NEW or DETACHED state in
1477
         * _documentStates since the UoW does not hold references to such
1478
         * objects and the object hash can be reused. More generally, because
1479
         * the state may "change" between NEW/DETACHED without the UoW being
1480
         * aware of it.
1481
         */
1482 4
        $id = $class->getIdentifierObject($document);
1483
1484 4
        if ($id === null) {
1485 3
            return self::STATE_NEW;
1486
        }
1487
1488
        // Check for a version field, if available, to avoid a DB lookup.
1489 2
        if ($class->isVersioned) {
1490
            return $class->getFieldValue($document, $class->versionField)
1491
                ? self::STATE_DETACHED
1492
                : self::STATE_NEW;
1493
        }
1494
1495
        // Last try before DB lookup: check the identity map.
1496 2
        if ($this->tryGetById($id, $class)) {
1497 1
            return self::STATE_DETACHED;
1498
        }
1499
1500
        // DB lookup
1501 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1502 1
            return self::STATE_DETACHED;
1503
        }
1504
1505 1
        return self::STATE_NEW;
1506
    }
1507
1508
    /**
1509
     * INTERNAL:
1510
     * Removes a document from the identity map. This effectively detaches the
1511
     * document from the persistence management of Doctrine.
1512
     *
1513
     * @ignore
1514
     * @param object $document
1515
     * @throws \InvalidArgumentException
1516
     * @return boolean
1517
     */
1518 91
    public function removeFromIdentityMap($document)
1519
    {
1520 91
        $oid = spl_object_hash($document);
1521
1522
        // Check if id is registered first
1523 91
        if ( ! isset($this->documentIdentifiers[$oid])) {
1524
            return false;
1525
        }
1526
1527 91
        $class = $this->dm->getClassMetadata(get_class($document));
1528 91
        $id = $this->getIdForIdentityMap($document);
1529
1530 91
        if (isset($this->identityMap[$class->name][$id])) {
1531 91
            unset($this->identityMap[$class->name][$id]);
1532 91
            $this->documentStates[$oid] = self::STATE_DETACHED;
1533 91
            return true;
1534
        }
1535
1536
        return false;
1537
    }
1538
1539
    /**
1540
     * INTERNAL:
1541
     * Gets a document in the identity map by its identifier hash.
1542
     *
1543
     * @ignore
1544
     * @param mixed         $id    Document identifier
1545
     * @param ClassMetadata $class Document class
1546
     * @return object
1547
     * @throws InvalidArgumentException if the class does not have an identifier
1548
     */
1549 34
    public function getById($id, ClassMetadata $class)
1550
    {
1551 34
        if ( ! $class->identifier) {
1552
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1553
        }
1554
1555 34
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1556
1557 34
        return $this->identityMap[$class->name][$serializedId];
1558
    }
1559
1560
    /**
1561
     * INTERNAL:
1562
     * Tries to get a document by its identifier hash. If no document is found
1563
     * for the given hash, FALSE is returned.
1564
     *
1565
     * @ignore
1566
     * @param mixed         $id    Document identifier
1567
     * @param ClassMetadata $class Document class
1568
     * @return mixed The found document or FALSE.
1569
     * @throws InvalidArgumentException if the class does not have an identifier
1570
     */
1571 316
    public function tryGetById($id, ClassMetadata $class)
1572
    {
1573 316
        if ( ! $class->identifier) {
1574
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1575
        }
1576
1577 316
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1578
1579 316
        return isset($this->identityMap[$class->name][$serializedId]) ?
1580 316
            $this->identityMap[$class->name][$serializedId] : false;
1581
    }
1582
1583
    /**
1584
     * Schedules a document for dirty-checking at commit-time.
1585
     *
1586
     * @param object $document The document to schedule for dirty-checking.
1587
     * @todo Rename: scheduleForSynchronization
1588
     */
1589 3
    public function scheduleForDirtyCheck($document)
1590
    {
1591 3
        $class = $this->dm->getClassMetadata(get_class($document));
1592 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1593 3
    }
1594
1595
    /**
1596
     * Checks whether a document is registered in the identity map.
1597
     *
1598
     * @param object $document
1599
     * @return boolean
1600
     */
1601 89
    public function isInIdentityMap($document)
1602
    {
1603 89
        $oid = spl_object_hash($document);
1604
1605 89
        if ( ! isset($this->documentIdentifiers[$oid])) {
1606 6
            return false;
1607
        }
1608
1609 87
        $class = $this->dm->getClassMetadata(get_class($document));
1610 87
        $id = $this->getIdForIdentityMap($document);
1611
1612 87
        return isset($this->identityMap[$class->name][$id]);
1613
    }
1614
1615
    /**
1616
     * @param object $document
1617
     * @return string
1618
     */
1619 700
    private function getIdForIdentityMap($document)
1620
    {
1621 700
        $class = $this->dm->getClassMetadata(get_class($document));
1622
1623 700
        if ( ! $class->identifier) {
1624 164
            $id = spl_object_hash($document);
1625
        } else {
1626 699
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1627 699
            $id = serialize($class->getDatabaseIdentifierValue($id));
1628
        }
1629
1630 700
        return $id;
1631
    }
1632
1633
    /**
1634
     * INTERNAL:
1635
     * Checks whether an identifier exists in the identity map.
1636
     *
1637
     * @ignore
1638
     * @param string $id
1639
     * @param string $rootClassName
1640
     * @return boolean
1641
     */
1642
    public function containsId($id, $rootClassName)
1643
    {
1644
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1645
    }
1646
1647
    /**
1648
     * Persists a document as part of the current unit of work.
1649
     *
1650
     * @param object $document The document to persist.
1651
     * @throws MongoDBException If trying to persist MappedSuperclass.
1652
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1653
     */
1654 668
    public function persist($document)
1655
    {
1656 668
        $class = $this->dm->getClassMetadata(get_class($document));
1657 668
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1658 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1659
        }
1660 667
        $visited = array();
1661 667
        $this->doPersist($document, $visited);
1662 663
    }
1663
1664
    /**
1665
     * Saves a document as part of the current unit of work.
1666
     * This method is internally called during save() cascades as it tracks
1667
     * the already visited documents to prevent infinite recursions.
1668
     *
1669
     * NOTE: This method always considers documents that are not yet known to
1670
     * this UnitOfWork as NEW.
1671
     *
1672
     * @param object $document The document to persist.
1673
     * @param array $visited The already visited documents.
1674
     * @throws \InvalidArgumentException
1675
     * @throws MongoDBException
1676
     */
1677 667
    private function doPersist($document, array &$visited)
1678
    {
1679 667
        $oid = spl_object_hash($document);
1680 667
        if (isset($visited[$oid])) {
1681 24
            return; // Prevent infinite recursion
1682
        }
1683
1684 667
        $visited[$oid] = $document; // Mark visited
1685
1686 667
        $class = $this->dm->getClassMetadata(get_class($document));
1687
1688 667
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1689
        switch ($documentState) {
1690 667
            case self::STATE_MANAGED:
1691
                // Nothing to do, except if policy is "deferred explicit"
1692 61
                if ($class->isChangeTrackingDeferredExplicit()) {
1693
                    $this->scheduleForDirtyCheck($document);
1694
                }
1695 61
                break;
1696 665
            case self::STATE_NEW:
1697 665
                $this->persistNew($class, $document);
1698 663
                break;
1699
1700 2
            case self::STATE_REMOVED:
1701
                // Document becomes managed again
1702 2
                unset($this->documentDeletions[$oid]);
1703
1704 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1705 2
                break;
1706
1707
            case self::STATE_DETACHED:
1708
                throw new \InvalidArgumentException(
1709
                    'Behavior of persist() for a detached document is not yet defined.');
1710
1711
            default:
1712
                throw MongoDBException::invalidDocumentState($documentState);
1713
        }
1714
1715 665
        $this->cascadePersist($document, $visited);
1716 663
    }
1717
1718
    /**
1719
     * Deletes a document as part of the current unit of work.
1720
     *
1721
     * @param object $document The document to remove.
1722
     */
1723 77
    public function remove($document)
1724
    {
1725 77
        $visited = array();
1726 77
        $this->doRemove($document, $visited);
1727 77
    }
1728
1729
    /**
1730
     * Deletes a document as part of the current unit of work.
1731
     *
1732
     * This method is internally called during delete() cascades as it tracks
1733
     * the already visited documents to prevent infinite recursions.
1734
     *
1735
     * @param object $document The document to delete.
1736
     * @param array $visited The map of the already visited documents.
1737
     * @throws MongoDBException
1738
     */
1739 77
    private function doRemove($document, array &$visited)
1740
    {
1741 77
        $oid = spl_object_hash($document);
1742 77
        if (isset($visited[$oid])) {
1743 1
            return; // Prevent infinite recursion
1744
        }
1745
1746 77
        $visited[$oid] = $document; // mark visited
1747
1748
        /* Cascade first, because scheduleForDelete() removes the entity from
1749
         * the identity map, which can cause problems when a lazy Proxy has to
1750
         * be initialized for the cascade operation.
1751
         */
1752 77
        $this->cascadeRemove($document, $visited);
1753
1754 77
        $class = $this->dm->getClassMetadata(get_class($document));
1755 77
        $documentState = $this->getDocumentState($document);
1756
        switch ($documentState) {
1757 77
            case self::STATE_NEW:
1758 77
            case self::STATE_REMOVED:
1759
                // nothing to do
1760 1
                break;
1761 77
            case self::STATE_MANAGED:
1762 77
                $this->lifecycleEventManager->preRemove($class, $document);
1763 77
                $this->scheduleForDelete($document);
1764 77
                break;
1765
            case self::STATE_DETACHED:
1766
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1767
            default:
1768
                throw MongoDBException::invalidDocumentState($documentState);
1769
        }
1770 77
    }
1771
1772
    /**
1773
     * Merges the state of the given detached document into this UnitOfWork.
1774
     *
1775
     * @param object $document
1776
     * @return object The managed copy of the document.
1777
     */
1778 15
    public function merge($document)
1779
    {
1780 15
        $visited = array();
1781
1782 15
        return $this->doMerge($document, $visited);
1783
    }
1784
1785
    /**
1786
     * Executes a merge operation on a document.
1787
     *
1788
     * @param object      $document
1789
     * @param array       $visited
1790
     * @param object|null $prevManagedCopy
1791
     * @param array|null  $assoc
1792
     *
1793
     * @return object The managed copy of the document.
1794
     *
1795
     * @throws InvalidArgumentException If the entity instance is NEW.
1796
     * @throws LockException If the document uses optimistic locking through a
1797
     *                       version attribute and the version check against the
1798
     *                       managed copy fails.
1799
     */
1800 15
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1801
    {
1802 15
        $oid = spl_object_hash($document);
1803
1804 15
        if (isset($visited[$oid])) {
1805 1
            return $visited[$oid]; // Prevent infinite recursion
1806
        }
1807
1808 15
        $visited[$oid] = $document; // mark visited
1809
1810 15
        $class = $this->dm->getClassMetadata(get_class($document));
1811
1812
        /* First we assume DETACHED, although it can still be NEW but we can
1813
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1814
         * an identity, we need to fetch it from the DB anyway in order to
1815
         * merge. MANAGED documents are ignored by the merge operation.
1816
         */
1817 15
        $managedCopy = $document;
1818
1819 15
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1820 15
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1821
                $document->__load();
1822
            }
1823
1824 15
            $identifier = $class->getIdentifier();
1825
            // We always have one element in the identifier array but it might be null
1826 15
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1827 15
            $managedCopy = null;
1828
1829
            // Try to fetch document from the database
1830 15
            if (! $class->isEmbeddedDocument && $id !== null) {
1831 12
                $managedCopy = $this->dm->find($class->name, $id);
1832
1833
                // Managed copy may be removed in which case we can't merge
1834 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1835
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1836
                }
1837
1838 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...
1839
                    $managedCopy->__load();
1840
                }
1841
            }
1842
1843 15
            if ($managedCopy === null) {
1844
                // Create a new managed instance
1845 7
                $managedCopy = $class->newInstance();
1846 7
                if ($id !== null) {
1847 3
                    $class->setIdentifierValue($managedCopy, $id);
1848
                }
1849 7
                $this->persistNew($class, $managedCopy);
1850
            }
1851
1852 15
            if ($class->isVersioned) {
1853
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1854
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1855
1856
                // Throw exception if versions don't match
1857
                if ($managedCopyVersion != $documentVersion) {
1858
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1859
                }
1860
            }
1861
1862
            // Merge state of $document into existing (managed) document
1863 15
            foreach ($class->reflClass->getProperties() as $prop) {
1864 15
                $name = $prop->name;
1865 15
                $prop->setAccessible(true);
1866 15
                if ( ! isset($class->associationMappings[$name])) {
1867 15
                    if ( ! $class->isIdentifier($name)) {
1868 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1869
                    }
1870
                } else {
1871 15
                    $assoc2 = $class->associationMappings[$name];
1872
1873 15
                    if ($assoc2['type'] === 'one') {
1874 7
                        $other = $prop->getValue($document);
1875
1876 7
                        if ($other === null) {
1877 2
                            $prop->setValue($managedCopy, null);
1878 6
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1879
                            // Do not merge fields marked lazy that have not been fetched
1880 1
                            continue;
1881 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1882
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1883
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1884
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1885
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1886
                                $relatedId = $targetClass->getIdentifierObject($other);
1887
1888
                                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...
1889
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1890
                                } else {
1891
                                    $other = $this
1892
                                        ->dm
1893
                                        ->getProxyFactory()
1894
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1895
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
2055
            } else {
2056 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2057
            }
2058
        }
2059
2060 22
        $this->cascadeRefresh($document, $visited);
2061 22
    }
2062
2063
    /**
2064
     * Cascades a refresh operation to associated documents.
2065
     *
2066
     * @param object $document
2067
     * @param array $visited
2068
     */
2069 22
    private function cascadeRefresh($document, array &$visited)
2070
    {
2071 22
        $class = $this->dm->getClassMetadata(get_class($document));
2072
2073 22
        $associationMappings = array_filter(
2074 22
            $class->associationMappings,
2075
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2076
        );
2077
2078 22
        foreach ($associationMappings as $mapping) {
2079 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2080 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2081 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2082
                    // Unwrap so that foreach() does not initialize
2083 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2084
                }
2085 15
                foreach ($relatedDocuments as $relatedDocument) {
2086 15
                    $this->doRefresh($relatedDocument, $visited);
2087
                }
2088 10
            } elseif ($relatedDocuments !== null) {
2089 15
                $this->doRefresh($relatedDocuments, $visited);
2090
            }
2091
        }
2092 22
    }
2093
2094
    /**
2095
     * Cascades a detach operation to associated documents.
2096
     *
2097
     * @param object $document
2098
     * @param array $visited
2099
     */
2100 17 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2101
    {
2102 17
        $class = $this->dm->getClassMetadata(get_class($document));
2103 17
        foreach ($class->fieldMappings as $mapping) {
2104 17
            if ( ! $mapping['isCascadeDetach']) {
2105 17
                continue;
2106
            }
2107 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2108 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2109 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2110
                    // Unwrap so that foreach() does not initialize
2111 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2112
                }
2113 11
                foreach ($relatedDocuments as $relatedDocument) {
2114 11
                    $this->doDetach($relatedDocument, $visited);
2115
                }
2116 11
            } elseif ($relatedDocuments !== null) {
2117 11
                $this->doDetach($relatedDocuments, $visited);
2118
            }
2119
        }
2120 17
    }
2121
    /**
2122
     * Cascades a merge operation to associated documents.
2123
     *
2124
     * @param object $document
2125
     * @param object $managedCopy
2126
     * @param array $visited
2127
     */
2128 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2129
    {
2130 15
        $class = $this->dm->getClassMetadata(get_class($document));
2131
2132 15
        $associationMappings = array_filter(
2133 15
            $class->associationMappings,
2134
            function ($assoc) { return $assoc['isCascadeMerge']; }
2135
        );
2136
2137 15
        foreach ($associationMappings as $assoc) {
2138 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2139
2140 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2141 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2142
                    // Collections are the same, so there is nothing to do
2143 1
                    continue;
2144
                }
2145
2146 10
                foreach ($relatedDocuments as $relatedDocument) {
2147 10
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2148
                }
2149 7
            } elseif ($relatedDocuments !== null) {
2150 14
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2151
            }
2152
        }
2153 15
    }
2154
2155
    /**
2156
     * Cascades the save operation to associated documents.
2157
     *
2158
     * @param object $document
2159
     * @param array $visited
2160
     */
2161 665
    private function cascadePersist($document, array &$visited)
2162
    {
2163 665
        $class = $this->dm->getClassMetadata(get_class($document));
2164
2165 665
        $associationMappings = array_filter(
2166 665
            $class->associationMappings,
2167
            function ($assoc) { return $assoc['isCascadePersist']; }
2168
        );
2169
2170 665
        foreach ($associationMappings as $fieldName => $mapping) {
2171 456
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2172
2173 456
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2174 378
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2175 17
                    if ($relatedDocuments->getOwner() !== $document) {
2176 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2177
                    }
2178
                    // Unwrap so that foreach() does not initialize
2179 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2180
                }
2181
2182 378
                $count = 0;
2183 378
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2184 206
                    if ( ! empty($mapping['embedded'])) {
2185 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2186 126
                        if ($knownParent && $knownParent !== $document) {
2187 4
                            $relatedDocument = clone $relatedDocument;
2188 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2189
                        }
2190 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2191 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2192
                    }
2193 378
                    $this->doPersist($relatedDocument, $visited);
2194
                }
2195 364
            } elseif ($relatedDocuments !== null) {
2196 138
                if ( ! empty($mapping['embedded'])) {
2197 77
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2198 77
                    if ($knownParent && $knownParent !== $document) {
2199 6
                        $relatedDocuments = clone $relatedDocuments;
2200 6
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2201
                    }
2202 77
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2203
                }
2204 456
                $this->doPersist($relatedDocuments, $visited);
2205
            }
2206
        }
2207 663
    }
2208
2209
    /**
2210
     * Cascades the delete operation to associated documents.
2211
     *
2212
     * @param object $document
2213
     * @param array $visited
2214
     */
2215 77 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...
2216
    {
2217 77
        $class = $this->dm->getClassMetadata(get_class($document));
2218 77
        foreach ($class->fieldMappings as $mapping) {
2219 77
            if ( ! $mapping['isCascadeRemove']) {
2220 76
                continue;
2221
            }
2222 36
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2223 2
                $document->__load();
2224
            }
2225
2226 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2227 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2228
                // If its a PersistentCollection initialization is intended! No unwrap!
2229 25
                foreach ($relatedDocuments as $relatedDocument) {
2230 25
                    $this->doRemove($relatedDocument, $visited);
2231
                }
2232 24
            } elseif ($relatedDocuments !== null) {
2233 36
                $this->doRemove($relatedDocuments, $visited);
2234
            }
2235
        }
2236 77
    }
2237
2238
    /**
2239
     * Acquire a lock on the given document.
2240
     *
2241
     * @param object $document
2242
     * @param int $lockMode
2243
     * @param int $lockVersion
2244
     * @throws LockException
2245
     * @throws \InvalidArgumentException
2246
     */
2247 9
    public function lock($document, $lockMode, $lockVersion = null)
2248
    {
2249 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2250 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2251
        }
2252
2253 8
        $documentName = get_class($document);
2254 8
        $class = $this->dm->getClassMetadata($documentName);
2255
2256 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2257 3
            if ( ! $class->isVersioned) {
2258 1
                throw LockException::notVersioned($documentName);
2259
            }
2260
2261 2
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2262 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2263 2
                if ($documentVersion != $lockVersion) {
2264 2
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2265
                }
2266
            }
2267 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2268 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2269
        }
2270 6
    }
2271
2272
    /**
2273
     * Releases a lock on the given document.
2274
     *
2275
     * @param object $document
2276
     * @throws \InvalidArgumentException
2277
     */
2278 1
    public function unlock($document)
2279
    {
2280 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2281
            throw new \InvalidArgumentException("Document is not MANAGED.");
2282
        }
2283 1
        $documentName = get_class($document);
2284 1
        $this->getDocumentPersister($documentName)->unlock($document);
2285 1
    }
2286
2287
    /**
2288
     * Clears the UnitOfWork.
2289
     *
2290
     * @param string|null $documentName if given, only documents of this type will get detached.
2291
     */
2292 422
    public function clear($documentName = null)
2293
    {
2294 422
        if ($documentName === null) {
2295 414
            $this->identityMap =
2296 414
            $this->documentIdentifiers =
2297 414
            $this->originalDocumentData =
2298 414
            $this->documentChangeSets =
2299 414
            $this->documentStates =
2300 414
            $this->scheduledForDirtyCheck =
2301 414
            $this->documentInsertions =
2302 414
            $this->documentUpserts =
2303 414
            $this->documentUpdates =
2304 414
            $this->documentDeletions =
2305 414
            $this->collectionUpdates =
2306 414
            $this->collectionDeletions =
2307 414
            $this->parentAssociations =
2308 414
            $this->embeddedDocumentsRegistry =
2309 414
            $this->orphanRemovals =
2310 414
            $this->hasScheduledCollections = array();
2311
        } else {
2312 8
            $visited = array();
2313 8
            foreach ($this->identityMap as $className => $documents) {
2314 8
                if ($className === $documentName) {
2315 5
                    foreach ($documents as $document) {
2316 8
                        $this->doDetach($document, $visited);
2317
                    }
2318
                }
2319
            }
2320
        }
2321
2322 422 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...
2323
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2324
        }
2325 422
    }
2326
2327
    /**
2328
     * INTERNAL:
2329
     * Schedules an embedded document for removal. The remove() operation will be
2330
     * invoked on that document at the beginning of the next commit of this
2331
     * UnitOfWork.
2332
     *
2333
     * @ignore
2334
     * @param object $document
2335
     */
2336 53
    public function scheduleOrphanRemoval($document)
2337
    {
2338 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2339 53
    }
2340
2341
    /**
2342
     * INTERNAL:
2343
     * Unschedules an embedded or referenced object for removal.
2344
     *
2345
     * @ignore
2346
     * @param object $document
2347
     */
2348 114
    public function unscheduleOrphanRemoval($document)
2349
    {
2350 114
        $oid = spl_object_hash($document);
2351 114
        if (isset($this->orphanRemovals[$oid])) {
2352 1
            unset($this->orphanRemovals[$oid]);
2353
        }
2354 114
    }
2355
2356
    /**
2357
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2358
     *  1) sets owner if it was cloned
2359
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2360
     *  3) NOP if state is OK
2361
     * Returned collection should be used from now on (only important with 2nd point)
2362
     *
2363
     * @param PersistentCollectionInterface $coll
2364
     * @param object $document
2365
     * @param ClassMetadata $class
2366
     * @param string $propName
2367
     * @return PersistentCollectionInterface
2368
     */
2369 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2370
    {
2371 8
        $owner = $coll->getOwner();
2372 8
        if ($owner === null) { // cloned
2373 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2374 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2375 2
            if ( ! $coll->isInitialized()) {
2376 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2377
            }
2378 2
            $newValue = clone $coll;
2379 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2380 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2381 2
            if ($this->isScheduledForUpdate($document)) {
2382
                // @todo following line should be superfluous once collections are stored in change sets
2383
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2384
            }
2385 2
            return $newValue;
2386
        }
2387 6
        return $coll;
2388
    }
2389
2390
    /**
2391
     * INTERNAL:
2392
     * Schedules a complete collection for removal when this UnitOfWork commits.
2393
     *
2394
     * @param PersistentCollectionInterface $coll
2395
     */
2396 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2397
    {
2398 43
        $oid = spl_object_hash($coll);
2399 43
        unset($this->collectionUpdates[$oid]);
2400 43
        if ( ! isset($this->collectionDeletions[$oid])) {
2401 43
            $this->collectionDeletions[$oid] = $coll;
2402 43
            $this->scheduleCollectionOwner($coll);
2403
        }
2404 43
    }
2405
2406
    /**
2407
     * Checks whether a PersistentCollection is scheduled for deletion.
2408
     *
2409
     * @param PersistentCollectionInterface $coll
2410
     * @return boolean
2411
     */
2412 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2413
    {
2414 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2415
    }
2416
2417
    /**
2418
     * INTERNAL:
2419
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2420
     *
2421
     * @param PersistentCollectionInterface $coll
2422
     */
2423 232 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2424
    {
2425 232
        $oid = spl_object_hash($coll);
2426 232
        if (isset($this->collectionDeletions[$oid])) {
2427 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2428 12
            unset($this->collectionDeletions[$oid]);
2429 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2430
        }
2431 232
    }
2432
2433
    /**
2434
     * INTERNAL:
2435
     * Schedules a collection for update when this UnitOfWork commits.
2436
     *
2437
     * @param PersistentCollectionInterface $coll
2438
     */
2439 254
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2440
    {
2441 254
        $mapping = $coll->getMapping();
2442 254
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2443
            /* There is no need to $unset collection if it will be $set later
2444
             * This is NOP if collection is not scheduled for deletion
2445
             */
2446 41
            $this->unscheduleCollectionDeletion($coll);
2447
        }
2448 254
        $oid = spl_object_hash($coll);
2449 254
        if ( ! isset($this->collectionUpdates[$oid])) {
2450 254
            $this->collectionUpdates[$oid] = $coll;
2451 254
            $this->scheduleCollectionOwner($coll);
2452
        }
2453 254
    }
2454
2455
    /**
2456
     * INTERNAL:
2457
     * Unschedules a collection from being updated when this UnitOfWork commits.
2458
     *
2459
     * @param PersistentCollectionInterface $coll
2460
     */
2461 232 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2462
    {
2463 232
        $oid = spl_object_hash($coll);
2464 232
        if (isset($this->collectionUpdates[$oid])) {
2465 222
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2466 222
            unset($this->collectionUpdates[$oid]);
2467 222
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2468
        }
2469 232
    }
2470
2471
    /**
2472
     * Checks whether a PersistentCollection is scheduled for update.
2473
     *
2474
     * @param PersistentCollectionInterface $coll
2475
     * @return boolean
2476
     */
2477 133
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2478
    {
2479 133
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2480
    }
2481
2482
    /**
2483
     * INTERNAL:
2484
     * Gets PersistentCollections that have been visited during computing change
2485
     * set of $document
2486
     *
2487
     * @param object $document
2488
     * @return PersistentCollectionInterface[]
2489
     */
2490 616
    public function getVisitedCollections($document)
2491
    {
2492 616
        $oid = spl_object_hash($document);
2493 616
        return isset($this->visitedCollections[$oid])
2494 257
                ? $this->visitedCollections[$oid]
2495 616
                : array();
2496
    }
2497
2498
    /**
2499
     * INTERNAL:
2500
     * Gets PersistentCollections that are scheduled to update and related to $document
2501
     *
2502
     * @param object $document
2503
     * @return array
2504
     */
2505 617
    public function getScheduledCollections($document)
2506
    {
2507 617
        $oid = spl_object_hash($document);
2508 617
        return isset($this->hasScheduledCollections[$oid])
2509 255
                ? $this->hasScheduledCollections[$oid]
2510 617
                : array();
2511
    }
2512
2513
    /**
2514
     * Checks whether the document is related to a PersistentCollection
2515
     * scheduled for update or deletion.
2516
     *
2517
     * @param object $document
2518
     * @return boolean
2519
     */
2520 52
    public function hasScheduledCollections($document)
2521
    {
2522 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2523
    }
2524
2525
    /**
2526
     * Marks the PersistentCollection's top-level owner as having a relation to
2527
     * a collection scheduled for update or deletion.
2528
     *
2529
     * If the owner is not scheduled for any lifecycle action, it will be
2530
     * scheduled for update to ensure that versioning takes place if necessary.
2531
     *
2532
     * If the collection is nested within atomic collection, it is immediately
2533
     * unscheduled and atomic one is scheduled for update instead. This makes
2534
     * calculating update data way easier.
2535
     *
2536
     * @param PersistentCollectionInterface $coll
2537
     */
2538 256
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2539
    {
2540 256
        $document = $this->getOwningDocument($coll->getOwner());
2541 256
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2542
2543 256
        if ($document !== $coll->getOwner()) {
2544 25
            $parent = $coll->getOwner();
2545 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2546 25
                list($mapping, $parent, ) = $parentAssoc;
2547
            }
2548 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2549 8
                $class = $this->dm->getClassMetadata(get_class($document));
2550 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
0 ignored issues
show
Bug introduced by
The variable $mapping does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

Loading history...
2580
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2581
            }
2582
2583 40
            list(, $document, ) = $parentAssociation;
2584 40
            $class = $this->dm->getClassMetadata(get_class($document));
2585
        }
2586
2587 258
        return $document;
2588
    }
2589
2590
    /**
2591
     * Gets the class name for an association (embed or reference) with respect
2592
     * to any discriminator value.
2593
     *
2594
     * @param array      $mapping Field mapping for the association
2595
     * @param array|null $data    Data for the embedded document or reference
2596
     * @return string Class name.
2597
     */
2598 228
    public function getClassNameForAssociation(array $mapping, $data)
2599
    {
2600 228
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2601
2602 228
        $discriminatorValue = null;
2603 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2604 21
            $discriminatorValue = $data[$discriminatorField];
2605 208
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2606
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2607
        }
2608
2609 228
        if ($discriminatorValue !== null) {
2610 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2611 10
                ? $mapping['discriminatorMap'][$discriminatorValue]
2612 21
                : $discriminatorValue;
2613
        }
2614
2615 208
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2616
2617 208 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2618 15
            $discriminatorValue = $data[$class->discriminatorField];
2619 193
        } elseif ($class->defaultDiscriminatorValue !== null) {
2620 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2621
        }
2622
2623 208
        if ($discriminatorValue !== null) {
2624 16
            return isset($class->discriminatorMap[$discriminatorValue])
2625 14
                ? $class->discriminatorMap[$discriminatorValue]
2626 16
                : $discriminatorValue;
2627
        }
2628
2629 192
        return $mapping['targetDocument'];
2630
    }
2631
2632
    /**
2633
     * INTERNAL:
2634
     * Creates a document. Used for reconstitution of documents during hydration.
2635
     *
2636
     * @ignore
2637
     * @param string $className The name of the document class.
2638
     * @param array $data The data for the document.
2639
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2640
     * @param object $document The document to be hydrated into in case of creation
2641
     * @return object The document instance.
2642
     * @internal Highly performance-sensitive method.
2643
     */
2644 426
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2645
    {
2646 426
        $class = $this->dm->getClassMetadata($className);
2647
2648
        // @TODO figure out how to remove this
2649 426
        $discriminatorValue = null;
2650 426 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...
2651 19
            $discriminatorValue = $data[$class->discriminatorField];
2652 418
        } elseif (isset($class->defaultDiscriminatorValue)) {
2653 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2654
        }
2655
2656 426
        if ($discriminatorValue !== null) {
2657 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2658 18
                ? $class->discriminatorMap[$discriminatorValue]
2659 20
                : $discriminatorValue;
2660
2661 20
            $class = $this->dm->getClassMetadata($className);
2662
2663 20
            unset($data[$class->discriminatorField]);
2664
        }
2665
        
2666 426
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2667 2
            $document = $class->newInstance();
2668 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2669 2
            return $document;
2670
        }
2671
2672 425
        $isManagedObject = false;
2673 425
        if (! $class->isQueryResultDocument) {
2674 425
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2675 425
            $serializedId = serialize($id);
2676 425
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2677
        }
2678
2679 425
        if ($isManagedObject) {
2680 110
            $document = $this->identityMap[$class->name][$serializedId];
0 ignored issues
show
Bug introduced by
The variable $serializedId does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2681 110
            $oid = spl_object_hash($document);
2682 110
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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