Completed
Pull Request — master (#1519)
by Maciej
18:24
created

UnitOfWork::getDocumentChangeSet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\EventManager;
25
use Doctrine\Common\NotifyPropertyChanged;
26
use Doctrine\Common\PropertyChangedListener;
27
use Doctrine\MongoDB\GridFSFile;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
30
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
31
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\Query;
34
use Doctrine\ODM\MongoDB\Types\Type;
35
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
36
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
37
38
/**
39
 * The UnitOfWork is responsible for tracking changes to objects during an
40
 * "object-level" transaction and for writing out changes to the database
41
 * in the correct order.
42
 *
43
 * @since       1.0
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = array();
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = array();
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @var array
101
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
102
     *           A value will only really be copied if the value in the document is modified
103
     *           by the user.
104
     */
105
    private $originalDocumentData = array();
106
107
    /**
108
     * Map of document changes. Keys are object ids (spl_object_hash).
109
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
110
     *
111
     * @var array
112
     */
113
    private $documentChangeSets = array();
114
115
    /**
116
     * The (cached) states of any known documents.
117
     * Keys are object ids (spl_object_hash).
118
     *
119
     * @var array
120
     */
121
    private $documentStates = array();
122
123
    /**
124
     * Map of documents that are scheduled for dirty checking at commit time.
125
     *
126
     * Documents are grouped by their class name, and then indexed by their SPL
127
     * object hash. This is only used for documents with a change tracking
128
     * policy of DEFERRED_EXPLICIT.
129
     *
130
     * @var array
131
     * @todo rename: scheduledForSynchronization
132
     */
133
    private $scheduledForDirtyCheck = array();
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = array();
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = array();
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = array();
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = array();
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = array();
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = array();
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = array();
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = array();
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = array();
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = array();
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents.
244
     *
245
     * @var array
246
     */
247
    private $parentAssociations = array();
248
249
    /**
250
     * @var LifecycleEventManager
251
     */
252
    private $lifecycleEventManager;
253
254
    /**
255
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
256
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
257
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
258
     *
259
     * @var array
260
     */
261
    private $embeddedDocumentsRegistry = array();
262
263
    /**
264
     * 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 1074
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
271
    {
272 1074
        $this->dm = $dm;
273 1074
        $this->evm = $evm;
274 1074
        $this->hydratorFactory = $hydratorFactory;
275 1074
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
276 1074
    }
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 745
    public function getPersistenceBuilder()
285
    {
286 745
        if ( ! $this->persistenceBuilder) {
287 745
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
288 745
        }
289 745
        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 199
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
301
    {
302 199
        $oid = spl_object_hash($document);
303 199
        $this->embeddedDocumentsRegistry[$oid] = $document;
304 199
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
305 199
    }
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 230
    public function getParentAssociation($document)
318
    {
319 230
        $oid = spl_object_hash($document);
320 230
        if ( ! isset($this->parentAssociations[$oid])) {
321 225
            return null;
322
        }
323 178
        return $this->parentAssociations[$oid];
324
    }
325
326
    /**
327
     * Get the document persister instance for the given document name
328
     *
329
     * @param string $documentName
330
     * @return Persisters\DocumentPersister
331
     */
332 743
    public function getDocumentPersister($documentName)
333
    {
334 743
        if ( ! isset($this->persisters[$documentName])) {
335 729
            $class = $this->dm->getClassMetadata($documentName);
336 729
            $pb = $this->getPersistenceBuilder();
337 729
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
338 729
        }
339 743
        return $this->persisters[$documentName];
340
    }
341
342
    /**
343
     * Get the collection persister instance.
344
     *
345
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
346
     */
347 743
    public function getCollectionPersister()
348
    {
349 743
        if ( ! isset($this->collectionPersister)) {
350 743
            $pb = $this->getPersistenceBuilder();
351 743
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
352 743
        }
353 743
        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 616
    public function commit($document = null, array $options = array())
382
    {
383
        // Raise preFlush
384 616
        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 616
        if ($document === null) {
390 610
            $this->computeChangeSets();
391 615
        } elseif (is_object($document)) {
392 13
            $this->computeSingleDocumentChangeSet($document);
393 13
        } elseif (is_array($document)) {
394 1
            foreach ($document as $object) {
395 1
                $this->computeSingleDocumentChangeSet($object);
396 1
            }
397 1
        }
398
399 614
        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 264
            $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 220
            $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 205
            $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 25
            $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
405 25
            $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 614
        ) {
407 25
            return; // Nothing to do.
408
        }
409
410 611
        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 50
            }
414 50
        }
415
416
        // Raise onFlush
417 611
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419 7
        }
420
421 611
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 87
            list($class, $documents) = $classAndDocuments;
423 87
            $this->executeUpserts($class, $documents, $options);
424 611
        }
425
426 611
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 535
            list($class, $documents) = $classAndDocuments;
428 535
            $this->executeInserts($class, $documents, $options);
429 610
        }
430
431 610
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 234
            list($class, $documents) = $classAndDocuments;
433 234
            $this->executeUpdates($class, $documents, $options);
434 610
        }
435
436 610
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 72
            list($class, $documents) = $classAndDocuments;
438 72
            $this->executeDeletions($class, $documents, $options);
439 610
        }
440
441
        // Raise postFlush
442 610
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 610
        $this->documentInsertions =
448 610
        $this->documentUpserts =
449 610
        $this->documentUpdates =
450 610
        $this->documentDeletions =
451 610
        $this->documentChangeSets =
452 610
        $this->collectionUpdates =
453 610
        $this->collectionDeletions =
454 610
        $this->visitedCollections =
455 610
        $this->scheduledForDirtyCheck =
456 610
        $this->orphanRemovals =
457 610
        $this->hasScheduledCollections = array();
458 610
    }
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 611
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 611
        if (empty($documents)) {
470 611
            return array();
471
        }
472 610
        $divided = array();
473 610
        $embeds = array();
474 610
        foreach ($documents as $oid => $d) {
475 610
            $className = get_class($d);
476 610
            if (isset($embeds[$className])) {
477 77
                continue;
478
            }
479 610
            if (isset($divided[$className])) {
480 149
                $divided[$className][1][$oid] = $d;
481 149
                continue;
482
            }
483 610
            $class = $this->dm->getClassMetadata($className);
484 610
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 180
                $embeds[$className] = true;
486 180
                continue;
487
            }
488 610
            if (empty($divided[$class->name])) {
489 610
                $divided[$class->name] = array($class, array($oid => $d));
490 610
            } else {
491 4
                $divided[$class->name][1][$oid] = $d;
492
            }
493 610
        }
494 610
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 618 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 618
        foreach ($this->documentInsertions as $document) {
505 545
            $class = $this->dm->getClassMetadata(get_class($document));
506 545
            if ( ! $class->isEmbeddedDocument) {
507 540
                $this->computeChangeSet($class, $document);
508 539
            }
509 617
        }
510 617
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 617 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 617
        foreach ($this->documentUpserts as $document) {
520 86
            $class = $this->dm->getClassMetadata(get_class($document));
521 86
            if ( ! $class->isEmbeddedDocument) {
522 86
                $this->computeChangeSet($class, $document);
523 86
            }
524 617
        }
525 617
    }
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 10
        }
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 13
        ) {
569 8
            $this->computeChangeSet($class, $document);
570 8
        }
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 611
    public function getDocumentChangeSet($document)
580
    {
581 611
        $oid = spl_object_hash($document);
582 611
        if (isset($this->documentChangeSets[$oid])) {
583 608
            return $this->documentChangeSets[$oid];
584
        }
585 62
        return array();
586
    }
587
588
    /**
589
     * INTERNAL:
590
     * Sets the changeset for a document.
591
     *
592
     * @param object $document
593
     * @param array $changeset
594
     */
595 1
    public function setDocumentChangeSet($document, $changeset)
596
    {
597 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
598 1
    }
599
600
    /**
601
     * Get a documents actual data, flattening all the objects to arrays.
602
     *
603
     * @param object $document
604
     * @return array
605
     */
606 618
    public function getDocumentActualData($document)
607
    {
608 618
        $class = $this->dm->getClassMetadata(get_class($document));
609 618
        $actualData = array();
610 618
        foreach ($class->reflFields as $name => $refProp) {
611 618
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 618
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 52
                continue;
615
            }
616 618
            $value = $refProp->getValue($document);
617 618
            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 618
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 618
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 396
                if ( ! $value instanceof Collection) {
625 133
                    $value = new ArrayCollection($value);
626 133
                }
627
628
                // Inject PersistentCollection
629 396
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
630 396
                $coll->setOwner($document, $mapping);
631 396
                $coll->setDirty( ! $value->isEmpty());
632 396
                $class->reflFields[$name]->setValue($document, $coll);
633 396
                $actualData[$name] = $coll;
634 396
            } else {
635 618
                $actualData[$name] = $value;
636
            }
637 618
        }
638 618
        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 615
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 615
        if ( ! $class->isInheritanceTypeNone()) {
668 182
            $class = $this->dm->getClassMetadata(get_class($document));
669 182
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 615 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 11
        }
675
676 615
        $this->computeOrRecomputeChangeSet($class, $document);
677 614
    }
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 615
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 615
        $oid = spl_object_hash($document);
689 615
        $actualData = $this->getDocumentActualData($document);
690 615
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 615
        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 615
            $this->originalDocumentData[$oid] = $actualData;
695 615
            $changeSet = array();
696 615
            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 615
                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 615 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 187
                    continue;
707
                }
708 615
                $changeSet[$propName] = array(null, $actualValue);
709 615
            }
710 615
            $this->documentChangeSets[$oid] = $changeSet;
711 615
        } else {
712
            // Document is "fully" MANAGED: it was already fully persisted before
713
            // and we have a copy of the original data
714 294
            $originalData = $this->originalDocumentData[$oid];
715 294
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
716 294
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
717 2
                $changeSet = $this->documentChangeSets[$oid];
718 2
            } else {
719 294
                $changeSet = array();
720
            }
721
722 294
            foreach ($actualData as $propName => $actualValue) {
723
                // skip not saved fields
724 294
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
725
                    continue;
726
                }
727
728 294
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
729
730
                // skip if value has not changed
731 294
                if ($orgValue === $actualValue) {
732 293
                    if ($actualValue instanceof PersistentCollectionInterface) {
733 204
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
734
                            // consider dirty collections as changed as well
735 180
                            continue;
736
                        }
737 293
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
738
                        // but consider dirty GridFSFile instances as changed
739 293
                        continue;
740
                    }
741 102
                }
742
743
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
744 253
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
745 12
                    if ($orgValue !== null) {
746 8
                        $this->scheduleOrphanRemoval($orgValue);
747 8
                    }
748
749 12
                    $changeSet[$propName] = array($orgValue, $actualValue);
750 12
                    continue;
751
                }
752
753
                // if owning side of reference-one relationship
754 247
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
755 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
756 1
                        $this->scheduleOrphanRemoval($orgValue);
757 1
                    }
758
759 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
760 13
                    continue;
761
                }
762
763 240
                if ($isChangeTrackingNotify) {
764 3
                    continue;
765
                }
766
767
                // ignore inverse side of reference relationship
768 238 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...
769 6
                    continue;
770
                }
771
772
                // Persistent collection was exchanged with the "originally"
773
                // created one. This can only mean it was cloned and replaced
774
                // on another document.
775 236
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
776 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
777 6
                }
778
779
                // if embed-many or reference-many relationship
780 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
781 118
                    $changeSet[$propName] = array($orgValue, $actualValue);
782
                    /* If original collection was exchanged with a non-empty value
783
                     * and $set will be issued, there is no need to $unset it first
784
                     */
785 118
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
786 28
                        continue;
787
                    }
788 98
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
789 18
                        $this->scheduleCollectionDeletion($orgValue);
790 18
                    }
791 98
                    continue;
792
                }
793
794
                // skip equivalent date values
795 155
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
796 37
                    $dateType = Type::getType('date');
797 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
798 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
799
800 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
801 30
                        continue;
802
                    }
803 10
                }
804
805
                // regular field
806 138
                $changeSet[$propName] = array($orgValue, $actualValue);
807 294
            }
808 294
            if ($changeSet) {
809 242
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
810 242
                    ? $changeSet + $this->documentChangeSets[$oid]
811 21
                    : $changeSet;
812
813 242
                $this->originalDocumentData[$oid] = $actualData;
814 242
                $this->scheduleForUpdate($document);
815 242
            }
816
        }
817
818
        // Look for changes in associations of the document
819 615
        $associationMappings = array_filter(
820 615
            $class->associationMappings,
821
            function ($assoc) { return empty($assoc['notSaved']); }
822 615
        );
823
824 615
        foreach ($associationMappings as $mapping) {
825 466
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
826
827 466
            if ($value === null) {
828 314
                continue;
829
            }
830
831 453
            $this->computeAssociationChanges($document, $mapping, $value);
832
833 452
            if (isset($mapping['reference'])) {
834 342
                continue;
835
            }
836
837 350
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
838
839 350
            foreach ($values as $obj) {
840 184
                $oid2 = spl_object_hash($obj);
841
842 184
                if (isset($this->documentChangeSets[$oid2])) {
843 182
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
844
                        // instance of $value is the same as it was previously otherwise there would be
845
                        // change set already in place
846 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
847 40
                    }
848
849 182
                    if ( ! $isNewDocument) {
850 79
                        $this->scheduleForUpdate($document);
851 79
                    }
852
853 182
                    break;
854
                }
855 350
            }
856 614
        }
857 614
    }
858
859
    /**
860
     * Computes all the changes that have been done to documents and collections
861
     * since the last commit and stores these changes in the _documentChangeSet map
862
     * temporarily for access by the persisters, until the UoW commit is finished.
863
     */
864 613
    public function computeChangeSets()
865
    {
866 613
        $this->computeScheduleInsertsChangeSets();
867 612
        $this->computeScheduleUpsertsChangeSets();
868
869
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
870 612
        foreach ($this->identityMap as $className => $documents) {
871 612
            $class = $this->dm->getClassMetadata($className);
872 612
            if ($class->isEmbeddedDocument) {
873
                /* we do not want to compute changes to embedded documents up front
874
                 * in case embedded document was replaced and its changeset
875
                 * would corrupt data. Embedded documents' change set will
876
                 * be calculated by reachability from owning document.
877
                 */
878 173
                continue;
879
            }
880
881
            // If change tracking is explicit or happens through notification, then only compute
882
            // changes on document of that type that are explicitly marked for synchronization.
883 612
            switch (true) {
884 612
                case ($class->isChangeTrackingDeferredImplicit()):
885 611
                    $documentsToProcess = $documents;
886 611
                    break;
887
888 4
                case (isset($this->scheduledForDirtyCheck[$className])):
889 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
890 3
                    break;
891
892 4
                default:
893 4
                    $documentsToProcess = array();
894
895 4
            }
896
897 612
            foreach ($documentsToProcess as $document) {
898
                // Ignore uninitialized proxy objects
899 608
                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...
900 10
                    continue;
901
                }
902
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
903 608
                $oid = spl_object_hash($document);
904 608 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...
905 608
                    && ! isset($this->documentUpserts[$oid])
906 608
                    && ! isset($this->documentDeletions[$oid])
907 608
                    && isset($this->documentStates[$oid])
908 608
                ) {
909 279
                    $this->computeChangeSet($class, $document);
910 279
                }
911 612
            }
912 612
        }
913 612
    }
914
915
    /**
916
     * Computes the changes of an association.
917
     *
918
     * @param object $parentDocument
919
     * @param array $assoc
920
     * @param mixed $value The value of the association.
921
     * @throws \InvalidArgumentException
922
     */
923 453
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
924
    {
925 453
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
926 453
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
927 453
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
928
929 453
        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...
930 8
            return;
931
        }
932
933 452
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
934 250
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
935 246
                $this->scheduleCollectionUpdate($value);
936 246
            }
937 250
            $topmostOwner = $this->getOwningDocument($value->getOwner());
938 250
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
939 250
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
940 146
                $value->initialize();
941 146
                foreach ($value->getDeletedDocuments() as $orphan) {
942 23
                    $this->scheduleOrphanRemoval($orphan);
943 146
                }
944 146
            }
945 250
        }
946
947
        // Look through the documents, and in any of their associations,
948
        // for transient (new) documents, recursively. ("Persistence by reachability")
949
        // Unwrap. Uninitialized collections will simply be empty.
950 452
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
951
952 452
        $count = 0;
953 452
        foreach ($unwrappedValue as $key => $entry) {
954 357
            if ( ! is_object($entry)) {
955 1
                throw new \InvalidArgumentException(
956 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
957 1
                );
958
            }
959
960 356
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
961
962 356
            $state = $this->getDocumentState($entry, self::STATE_NEW);
963
964
            // Handle "set" strategy for multi-level hierarchy
965 356
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
966 356
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
967
968 356
            $count++;
969
970
            switch ($state) {
971 356
                case self::STATE_NEW:
972 63
                    if ( ! $assoc['isCascadePersist']) {
973
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
974
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
975
                            . ' Explicitly persist the new document or configure cascading persist operations'
976
                            . ' on the relationship.');
977
                    }
978
979 63
                    $this->persistNew($targetClass, $entry);
980 63
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
981 63
                    $this->computeChangeSet($targetClass, $entry);
982 63
                    break;
983
984 351
                case self::STATE_MANAGED:
985 351
                    if ($targetClass->isEmbeddedDocument) {
986 175
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
987 175
                        if ($knownParent && $knownParent !== $parentDocument) {
988 7
                            $entry = clone $entry;
989 7
                            if ($assoc['type'] === ClassMetadata::ONE) {
990 4
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
991 4
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
992 4
                            } else {
993
                                // must use unwrapped value to not trigger orphan removal
994 6
                                $unwrappedValue[$key] = $entry;
995
                            }
996 7
                            $this->persistNew($targetClass, $entry);
997 7
                        }
998 175
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
999 175
                        $this->computeChangeSet($targetClass, $entry);
1000 175
                    }
1001 351
                    break;
1002
1003 1
                case self::STATE_REMOVED:
1004
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1005
                    // and remove the element from Collection.
1006 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1007
                        unset($value[$key]);
1008
                    }
1009 1
                    break;
1010
1011
                case self::STATE_DETACHED:
1012
                    // Can actually not happen right now as we assume STATE_NEW,
1013
                    // so the exception will be raised from the DBAL layer (constraint violation).
1014
                    throw new \InvalidArgumentException('A detached document was found through a '
1015
                        . 'relationship during cascading a persist operation.');
1016
1017
                default:
1018
                    // MANAGED associated documents are already taken into account
1019
                    // during changeset calculation anyway, since they are in the identity map.
1020
1021
            }
1022 451
        }
1023 451
    }
1024
1025
    /**
1026
     * INTERNAL:
1027
     * Computes the changeset of an individual document, independently of the
1028
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1029
     *
1030
     * The passed document must be a managed document. If the document already has a change set
1031
     * because this method is invoked during a commit cycle then the change sets are added.
1032
     * whereby changes detected in this method prevail.
1033
     *
1034
     * @ignore
1035
     * @param ClassMetadata $class The class descriptor of the document.
1036
     * @param object $document The document for which to (re)calculate the change set.
1037
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1038
     */
1039 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1040
    {
1041
        // Ignore uninitialized proxy objects
1042 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1043 1
            return;
1044
        }
1045
1046 19
        $oid = spl_object_hash($document);
1047
1048 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1049
            throw new \InvalidArgumentException('Document must be managed.');
1050
        }
1051
1052 19
        if ( ! $class->isInheritanceTypeNone()) {
1053 2
            $class = $this->dm->getClassMetadata(get_class($document));
1054 2
        }
1055
1056 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1057 19
    }
1058
1059
    /**
1060
     * @param ClassMetadata $class
1061
     * @param object $document
1062
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1063
     */
1064 644
    private function persistNew(ClassMetadata $class, $document)
1065
    {
1066 644
        $this->lifecycleEventManager->prePersist($class, $document);
1067 644
        $oid = spl_object_hash($document);
1068 644
        $upsert = false;
1069 644
        if ($class->identifier) {
1070 644
            $idValue = $class->getIdentifierValue($document);
1071 644
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1072
1073 644
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1074 3
                throw new \InvalidArgumentException(sprintf(
1075 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1076 3
                    get_class($document)
1077 3
                ));
1078
            }
1079
1080 643
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1081 1
                throw new \InvalidArgumentException(sprintf(
1082 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1083 1
                    get_class($document)
1084 1
                ));
1085
            }
1086
1087 642
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1088 561
                $idValue = $class->idGenerator->generate($this->dm, $document);
1089 561
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1090 561
                $class->setIdentifierValue($document, $idValue);
1091 561
            }
1092
1093 642
            $this->documentIdentifiers[$oid] = $idValue;
1094 642
        } else {
1095
            // this is for embedded documents without identifiers
1096 158
            $this->documentIdentifiers[$oid] = $oid;
1097
        }
1098
1099 642
        $this->documentStates[$oid] = self::STATE_MANAGED;
1100
1101 642
        if ($upsert) {
1102 91
            $this->scheduleForUpsert($class, $document);
1103 91
        } else {
1104 568
            $this->scheduleForInsert($class, $document);
1105
        }
1106 642
    }
1107
1108
    /**
1109
     * Executes all document insertions for documents of the specified type.
1110
     *
1111
     * @param ClassMetadata $class
1112
     * @param array $documents Array of documents to insert
1113
     * @param array $options Array of options to be used with batchInsert()
1114
     */
1115 535 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...
1116
    {
1117 535
        $persister = $this->getDocumentPersister($class->name);
1118
1119 535
        foreach ($documents as $oid => $document) {
1120 535
            $persister->addInsert($document);
1121 535
            unset($this->documentInsertions[$oid]);
1122 535
        }
1123
1124 535
        $persister->executeInserts($options);
1125
1126 534
        foreach ($documents as $document) {
1127 534
            $this->lifecycleEventManager->postPersist($class, $document);
1128 534
        }
1129 534
    }
1130
1131
    /**
1132
     * Executes all document upserts for documents of the specified type.
1133
     *
1134
     * @param ClassMetadata $class
1135
     * @param array $documents Array of documents to upsert
1136
     * @param array $options Array of options to be used with batchInsert()
1137
     */
1138 87 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...
1139
    {
1140 87
        $persister = $this->getDocumentPersister($class->name);
1141
1142
1143 87
        foreach ($documents as $oid => $document) {
1144 87
            $persister->addUpsert($document);
1145 87
            unset($this->documentUpserts[$oid]);
1146 87
        }
1147
1148 87
        $persister->executeUpserts($options);
1149
1150 87
        foreach ($documents as $document) {
1151 87
            $this->lifecycleEventManager->postPersist($class, $document);
1152 87
        }
1153 87
    }
1154
1155
    /**
1156
     * Executes all document updates for documents of the specified type.
1157
     *
1158
     * @param Mapping\ClassMetadata $class
1159
     * @param array $documents Array of documents to update
1160
     * @param array $options Array of options to be used with update()
1161
     */
1162 234
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1163
    {
1164 234
        $className = $class->name;
1165 234
        $persister = $this->getDocumentPersister($className);
1166
1167 234
        foreach ($documents as $oid => $document) {
1168 234
            $this->lifecycleEventManager->preUpdate($class, $document);
1169
1170 234
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1171 232
                $persister->update($document, $options);
1172 226
            }
1173
1174 228
            unset($this->documentUpdates[$oid]);
1175
1176 228
            $this->lifecycleEventManager->postUpdate($class, $document);
1177 228
        }
1178 227
    }
1179
1180
    /**
1181
     * Executes all document deletions for documents of the specified type.
1182
     *
1183
     * @param ClassMetadata $class
1184
     * @param array $documents Array of documents to delete
1185
     * @param array $options Array of options to be used with remove()
1186
     */
1187 72
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1188
    {
1189 72
        $persister = $this->getDocumentPersister($class->name);
1190
1191 72
        foreach ($documents as $oid => $document) {
1192 72
            if ( ! $class->isEmbeddedDocument) {
1193 34
                $persister->delete($document, $options);
1194 32
            }
1195
            unset(
1196 70
                $this->documentDeletions[$oid],
1197 70
                $this->documentIdentifiers[$oid],
1198 70
                $this->originalDocumentData[$oid]
1199
            );
1200
1201
            // Clear snapshot information for any referenced PersistentCollection
1202
            // http://www.doctrine-project.org/jira/browse/MODM-95
1203 70
            foreach ($class->associationMappings as $fieldMapping) {
1204 45
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1205 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1206 27
                    if ($value instanceof PersistentCollectionInterface) {
1207 23
                        $value->clearSnapshot();
1208 23
                    }
1209 27
                }
1210 70
            }
1211
1212
            // Document with this $oid after deletion treated as NEW, even if the $oid
1213
            // is obtained by a new document because the old one went out of scope.
1214 70
            $this->documentStates[$oid] = self::STATE_NEW;
1215
1216 70
            $this->lifecycleEventManager->postRemove($class, $document);
1217 70
        }
1218 70
    }
1219
1220
    /**
1221
     * Schedules a document for insertion into the database.
1222
     * If the document already has an identifier, it will be added to the
1223
     * identity map.
1224
     *
1225
     * @param ClassMetadata $class
1226
     * @param object $document The document to schedule for insertion.
1227
     * @throws \InvalidArgumentException
1228
     */
1229 571
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1829
                    $managedCopy->__load();
1830
                }
1831 12
            }
1832
1833 15
            if ($managedCopy === null) {
1834
                // Create a new managed instance
1835 7
                $managedCopy = $class->newInstance();
1836 7
                if ($id !== null) {
1837 3
                    $class->setIdentifierValue($managedCopy, $id);
1838 3
                }
1839 7
                $this->persistNew($class, $managedCopy);
1840 7
            }
1841
1842 15
            if ($class->isVersioned) {
1843
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1844
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1845
1846
                // Throw exception if versions don't match
1847
                if ($managedCopyVersion != $documentVersion) {
1848
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1849
                }
1850
            }
1851
1852
            // Merge state of $document into existing (managed) document
1853 15
            foreach ($class->reflClass->getProperties() as $prop) {
1854 15
                $name = $prop->name;
1855 15
                $prop->setAccessible(true);
1856 15
                if ( ! isset($class->associationMappings[$name])) {
1857 15
                    if ( ! $class->isIdentifier($name)) {
1858 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1859 15
                    }
1860 15
                } else {
1861 15
                    $assoc2 = $class->associationMappings[$name];
1862
1863 15
                    if ($assoc2['type'] === 'one') {
1864 7
                        $other = $prop->getValue($document);
1865
1866 7
                        if ($other === null) {
1867 2
                            $prop->setValue($managedCopy, null);
1868 7
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1869
                            // Do not merge fields marked lazy that have not been fetched
1870 1
                            continue;
1871 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1872
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1873
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1874
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1875
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1876
                                $relatedId = $targetClass->getIdentifierObject($other);
1877
1878
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1879
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1880
                                } else {
1881
                                    $other = $this
1882
                                        ->dm
1883
                                        ->getProxyFactory()
1884
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1885
                                    $this->registerManaged($other, $relatedId, array());
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...
1886
                                }
1887
                            }
1888
1889
                            $prop->setValue($managedCopy, $other);
1890
                        }
1891 6
                    } else {
1892 12
                        $mergeCol = $prop->getValue($document);
1893
1894 12
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1895
                            /* Do not merge fields marked lazy that have not
1896
                             * been fetched. Keep the lazy persistent collection
1897
                             * of the managed copy.
1898
                             */
1899 3
                            continue;
1900
                        }
1901
1902 12
                        $managedCol = $prop->getValue($managedCopy);
1903
1904 12
                        if ( ! $managedCol) {
1905 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1906 3
                            $managedCol->setOwner($managedCopy, $assoc2);
1907 3
                            $prop->setValue($managedCopy, $managedCol);
1908 3
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1909 3
                        }
1910
1911
                        /* Note: do not process association's target documents.
1912
                         * They will be handled during the cascade. Initialize
1913
                         * and, if necessary, clear $managedCol for now.
1914
                         */
1915 12
                        if ($assoc2['isCascadeMerge']) {
1916 12
                            $managedCol->initialize();
1917
1918
                            // If $managedCol differs from the merged collection, clear and set dirty
1919 12
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1920 3
                                $managedCol->unwrap()->clear();
1921 3
                                $managedCol->setDirty(true);
1922
1923 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1924
                                    $this->scheduleForDirtyCheck($managedCopy);
1925
                                }
1926 3
                            }
1927 12
                        }
1928
                    }
1929
                }
1930
1931 15
                if ($class->isChangeTrackingNotify()) {
1932
                    // Just treat all properties as changed, there is no other choice.
1933
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1934
                }
1935 15
            }
1936
1937 15
            if ($class->isChangeTrackingDeferredExplicit()) {
1938
                $this->scheduleForDirtyCheck($document);
1939
            }
1940 15
        }
1941
1942 15
        if ($prevManagedCopy !== null) {
1943 8
            $assocField = $assoc['fieldName'];
1944 8
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1945
1946 8
            if ($assoc['type'] === 'one') {
1947 4
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1948 4
            } else {
1949 6
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1950
1951 6
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1952 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1953 1
                }
1954
            }
1955 8
        }
1956
1957
        // Mark the managed copy visited as well
1958 15
        $visited[spl_object_hash($managedCopy)] = true;
1959
1960 15
        $this->cascadeMerge($document, $managedCopy, $visited);
1961
1962 15
        return $managedCopy;
1963
    }
1964
1965
    /**
1966
     * Detaches a document from the persistence management. It's persistence will
1967
     * no longer be managed by Doctrine.
1968
     *
1969
     * @param object $document The document to detach.
1970
     */
1971 12
    public function detach($document)
1972
    {
1973 12
        $visited = array();
1974 12
        $this->doDetach($document, $visited);
1975 12
    }
1976
1977
    /**
1978
     * Executes a detach operation on the given document.
1979
     *
1980
     * @param object $document
1981
     * @param array $visited
1982
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1983
     */
1984 17
    private function doDetach($document, array &$visited)
1985
    {
1986 17
        $oid = spl_object_hash($document);
1987 17
        if (isset($visited[$oid])) {
1988 4
            return; // Prevent infinite recursion
1989
        }
1990
1991 17
        $visited[$oid] = $document; // mark visited
1992
1993 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1994 17
            case self::STATE_MANAGED:
1995 17
                $this->removeFromIdentityMap($document);
1996 17
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1997 17
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1998 17
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1999 17
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2000 17
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2001 17
                break;
2002 4
            case self::STATE_NEW:
2003 4
            case self::STATE_DETACHED:
2004 4
                return;
2005 17
        }
2006
2007 17
        $this->cascadeDetach($document, $visited);
2008 17
    }
2009
2010
    /**
2011
     * Refreshes the state of the given document from the database, overwriting
2012
     * any local, unpersisted changes.
2013
     *
2014
     * @param object $document The document to refresh.
2015
     * @throws \InvalidArgumentException If the document is not MANAGED.
2016
     */
2017 22
    public function refresh($document)
2018
    {
2019 22
        $visited = array();
2020 22
        $this->doRefresh($document, $visited);
2021 21
    }
2022
2023
    /**
2024
     * Executes a refresh operation on a document.
2025
     *
2026
     * @param object $document The document to refresh.
2027
     * @param array $visited The already visited documents during cascades.
2028
     * @throws \InvalidArgumentException If the document is not MANAGED.
2029
     */
2030 22
    private function doRefresh($document, array &$visited)
2031
    {
2032 22
        $oid = spl_object_hash($document);
2033 22
        if (isset($visited[$oid])) {
2034
            return; // Prevent infinite recursion
2035
        }
2036
2037 22
        $visited[$oid] = $document; // mark visited
2038
2039 22
        $class = $this->dm->getClassMetadata(get_class($document));
2040
2041 22
        if ( ! $class->isEmbeddedDocument) {
2042 22
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2043 21
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2044 21
                $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...
2045 21
            } else {
2046 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2047
            }
2048 21
        }
2049
2050 21
        $this->cascadeRefresh($document, $visited);
2051 21
    }
2052
2053
    /**
2054
     * Cascades a refresh operation to associated documents.
2055
     *
2056
     * @param object $document
2057
     * @param array $visited
2058
     */
2059 21
    private function cascadeRefresh($document, array &$visited)
2060
    {
2061 21
        $class = $this->dm->getClassMetadata(get_class($document));
2062
2063 21
        $associationMappings = array_filter(
2064 21
            $class->associationMappings,
2065
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2066 21
        );
2067
2068 21
        foreach ($associationMappings as $mapping) {
2069 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2070 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2071 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2072
                    // Unwrap so that foreach() does not initialize
2073 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2074 15
                }
2075 15
                foreach ($relatedDocuments as $relatedDocument) {
2076
                    $this->doRefresh($relatedDocument, $visited);
2077 15
                }
2078 15
            } elseif ($relatedDocuments !== null) {
2079 2
                $this->doRefresh($relatedDocuments, $visited);
2080 2
            }
2081 21
        }
2082 21
    }
2083
2084
    /**
2085
     * Cascades a detach operation to associated documents.
2086
     *
2087
     * @param object $document
2088
     * @param array $visited
2089
     */
2090 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...
2091
    {
2092 17
        $class = $this->dm->getClassMetadata(get_class($document));
2093 17
        foreach ($class->fieldMappings as $mapping) {
2094 17
            if ( ! $mapping['isCascadeDetach']) {
2095 17
                continue;
2096
            }
2097 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2098 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2099 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2100
                    // Unwrap so that foreach() does not initialize
2101 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2102 8
                }
2103 11
                foreach ($relatedDocuments as $relatedDocument) {
2104 9
                    $this->doDetach($relatedDocument, $visited);
2105 11
                }
2106 11
            } elseif ($relatedDocuments !== null) {
2107 9
                $this->doDetach($relatedDocuments, $visited);
2108 9
            }
2109 17
        }
2110 17
    }
2111
    /**
2112
     * Cascades a merge operation to associated documents.
2113
     *
2114
     * @param object $document
2115
     * @param object $managedCopy
2116
     * @param array $visited
2117
     */
2118 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2119
    {
2120 15
        $class = $this->dm->getClassMetadata(get_class($document));
2121
2122 15
        $associationMappings = array_filter(
2123 15
            $class->associationMappings,
2124
            function ($assoc) { return $assoc['isCascadeMerge']; }
2125 15
        );
2126
2127 15
        foreach ($associationMappings as $assoc) {
2128 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2129
2130 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2131 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2132
                    // Collections are the same, so there is nothing to do
2133 1
                    continue;
2134
                }
2135
2136 10
                foreach ($relatedDocuments as $relatedDocument) {
2137 6
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2138 10
                }
2139 14
            } elseif ($relatedDocuments !== null) {
2140 5
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2141 5
            }
2142 15
        }
2143 15
    }
2144
2145
    /**
2146
     * Cascades the save operation to associated documents.
2147
     *
2148
     * @param object $document
2149
     * @param array $visited
2150
     */
2151 638
    private function cascadePersist($document, array &$visited)
2152
    {
2153 638
        $class = $this->dm->getClassMetadata(get_class($document));
2154
2155 638
        $associationMappings = array_filter(
2156 638
            $class->associationMappings,
2157
            function ($assoc) { return $assoc['isCascadePersist']; }
2158 638
        );
2159
2160 638
        foreach ($associationMappings as $fieldName => $mapping) {
2161 437
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2162
2163 437
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2164 367
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2165 17
                    if ($relatedDocuments->getOwner() !== $document) {
2166 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2167 2
                    }
2168
                    // Unwrap so that foreach() does not initialize
2169 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2170 17
                }
2171
2172 367
                $count = 0;
2173 367
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2174 206
                    if ( ! empty($mapping['embedded'])) {
2175 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2176 126
                        if ($knownParent && $knownParent !== $document) {
2177 4
                            $relatedDocument = clone $relatedDocument;
2178 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2179 4
                        }
2180 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2181 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2182 126
                    }
2183 206
                    $this->doPersist($relatedDocument, $visited);
2184 366
                }
2185 437
            } elseif ($relatedDocuments !== null) {
2186 134
                if ( ! empty($mapping['embedded'])) {
2187 76
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2188 76
                    if ($knownParent && $knownParent !== $document) {
2189 5
                        $relatedDocuments = clone $relatedDocuments;
2190 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2191 5
                    }
2192 76
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2193 76
                }
2194 134
                $this->doPersist($relatedDocuments, $visited);
2195 133
            }
2196 637
        }
2197 636
    }
2198
2199
    /**
2200
     * Cascades the delete operation to associated documents.
2201
     *
2202
     * @param object $document
2203
     * @param array $visited
2204
     */
2205 76 View Code Duplication
    private function cascadeRemove($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2206
    {
2207 76
        $class = $this->dm->getClassMetadata(get_class($document));
2208 76
        foreach ($class->fieldMappings as $mapping) {
2209 76
            if ( ! $mapping['isCascadeRemove']) {
2210 75
                continue;
2211
            }
2212 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...
2213 2
                $document->__load();
2214 2
            }
2215
2216 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2217 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2218
                // If its a PersistentCollection initialization is intended! No unwrap!
2219 25
                foreach ($relatedDocuments as $relatedDocument) {
2220 14
                    $this->doRemove($relatedDocument, $visited);
2221 25
                }
2222 36
            } elseif ($relatedDocuments !== null) {
2223 13
                $this->doRemove($relatedDocuments, $visited);
2224 13
            }
2225 76
        }
2226 76
    }
2227
2228
    /**
2229
     * Acquire a lock on the given document.
2230
     *
2231
     * @param object $document
2232
     * @param int $lockMode
2233
     * @param int $lockVersion
2234
     * @throws LockException
2235
     * @throws \InvalidArgumentException
2236
     */
2237 9
    public function lock($document, $lockMode, $lockVersion = null)
2238
    {
2239 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2240 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2241
        }
2242
2243 8
        $documentName = get_class($document);
2244 8
        $class = $this->dm->getClassMetadata($documentName);
2245
2246 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2247 3
            if ( ! $class->isVersioned) {
2248 1
                throw LockException::notVersioned($documentName);
2249
            }
2250
2251 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...
2252 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2253 2
                if ($documentVersion != $lockVersion) {
2254 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2255
                }
2256 1
            }
2257 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2258 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2259 5
        }
2260 6
    }
2261
2262
    /**
2263
     * Releases a lock on the given document.
2264
     *
2265
     * @param object $document
2266
     * @throws \InvalidArgumentException
2267
     */
2268 1
    public function unlock($document)
2269
    {
2270 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2271
            throw new \InvalidArgumentException("Document is not MANAGED.");
2272
        }
2273 1
        $documentName = get_class($document);
2274 1
        $this->getDocumentPersister($documentName)->unlock($document);
2275 1
    }
2276
2277
    /**
2278
     * Clears the UnitOfWork.
2279
     *
2280
     * @param string|null $documentName if given, only documents of this type will get detached.
2281
     */
2282 412
    public function clear($documentName = null)
2283
    {
2284 412
        if ($documentName === null) {
2285 404
            $this->identityMap =
2286 404
            $this->documentIdentifiers =
2287 404
            $this->originalDocumentData =
2288 404
            $this->documentChangeSets =
2289 404
            $this->documentStates =
2290 404
            $this->scheduledForDirtyCheck =
2291 404
            $this->documentInsertions =
2292 404
            $this->documentUpserts =
2293 404
            $this->documentUpdates =
2294 404
            $this->documentDeletions =
2295 404
            $this->collectionUpdates =
2296 404
            $this->collectionDeletions =
2297 404
            $this->parentAssociations =
2298 404
            $this->embeddedDocumentsRegistry =
2299 404
            $this->orphanRemovals =
2300 404
            $this->hasScheduledCollections = array();
2301 404
        } else {
2302 8
            $visited = array();
2303 8
            foreach ($this->identityMap as $className => $documents) {
2304 8
                if ($className === $documentName) {
2305 5
                    foreach ($documents as $document) {
2306 5
                        $this->doDetach($document, $visited);
2307 5
                    }
2308 5
                }
2309 8
            }
2310
        }
2311
2312 412 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...
2313
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2314
        }
2315 412
    }
2316
2317
    /**
2318
     * INTERNAL:
2319
     * Schedules an embedded document for removal. The remove() operation will be
2320
     * invoked on that document at the beginning of the next commit of this
2321
     * UnitOfWork.
2322
     *
2323
     * @ignore
2324
     * @param object $document
2325
     */
2326 53
    public function scheduleOrphanRemoval($document)
2327
    {
2328 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2329 53
    }
2330
2331
    /**
2332
     * INTERNAL:
2333
     * Unschedules an embedded or referenced object for removal.
2334
     *
2335
     * @ignore
2336
     * @param object $document
2337
     */
2338 113
    public function unscheduleOrphanRemoval($document)
2339
    {
2340 113
        $oid = spl_object_hash($document);
2341 113
        if (isset($this->orphanRemovals[$oid])) {
2342 1
            unset($this->orphanRemovals[$oid]);
2343 1
        }
2344 113
    }
2345
2346
    /**
2347
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2348
     *  1) sets owner if it was cloned
2349
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2350
     *  3) NOP if state is OK
2351
     * Returned collection should be used from now on (only important with 2nd point)
2352
     *
2353
     * @param PersistentCollectionInterface $coll
2354
     * @param object $document
2355
     * @param ClassMetadata $class
2356
     * @param string $propName
2357
     * @return PersistentCollectionInterface
2358
     */
2359 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2360
    {
2361 8
        $owner = $coll->getOwner();
2362 8
        if ($owner === null) { // cloned
2363 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2364 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2365 2
            if ( ! $coll->isInitialized()) {
2366 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2367 1
            }
2368 2
            $newValue = clone $coll;
2369 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2370 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2371 2
            if ($this->isScheduledForUpdate($document)) {
2372
                // @todo following line should be superfluous once collections are stored in change sets
2373
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2374
            }
2375 2
            return $newValue;
2376
        }
2377 6
        return $coll;
2378
    }
2379
2380
    /**
2381
     * INTERNAL:
2382
     * Schedules a complete collection for removal when this UnitOfWork commits.
2383
     *
2384
     * @param PersistentCollectionInterface $coll
2385
     */
2386 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2387
    {
2388 43
        $oid = spl_object_hash($coll);
2389 43
        unset($this->collectionUpdates[$oid]);
2390 43
        if ( ! isset($this->collectionDeletions[$oid])) {
2391 43
            $this->collectionDeletions[$oid] = $coll;
2392 43
            $this->scheduleCollectionOwner($coll);
2393 43
        }
2394 43
    }
2395
2396
    /**
2397
     * Checks whether a PersistentCollection is scheduled for deletion.
2398
     *
2399
     * @param PersistentCollectionInterface $coll
2400
     * @return boolean
2401
     */
2402 219
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2403
    {
2404 219
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2405
    }
2406
2407
    /**
2408
     * INTERNAL:
2409
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2410
     *
2411
     * @param PersistentCollectionInterface $coll
2412
     */
2413 225 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...
2414
    {
2415 225
        $oid = spl_object_hash($coll);
2416 225
        if (isset($this->collectionDeletions[$oid])) {
2417 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2418 12
            unset($this->collectionDeletions[$oid]);
2419 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2420 12
        }
2421 225
    }
2422
2423
    /**
2424
     * INTERNAL:
2425
     * Schedules a collection for update when this UnitOfWork commits.
2426
     *
2427
     * @param PersistentCollectionInterface $coll
2428
     */
2429 246
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2430
    {
2431 246
        $mapping = $coll->getMapping();
2432 246
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2433
            /* There is no need to $unset collection if it will be $set later
2434
             * This is NOP if collection is not scheduled for deletion
2435
             */
2436 41
            $this->unscheduleCollectionDeletion($coll);
2437 41
        }
2438 246
        $oid = spl_object_hash($coll);
2439 246
        if ( ! isset($this->collectionUpdates[$oid])) {
2440 246
            $this->collectionUpdates[$oid] = $coll;
2441 246
            $this->scheduleCollectionOwner($coll);
2442 246
        }
2443 246
    }
2444
2445
    /**
2446
     * INTERNAL:
2447
     * Unschedules a collection from being updated when this UnitOfWork commits.
2448
     *
2449
     * @param PersistentCollectionInterface $coll
2450
     */
2451 225 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...
2452
    {
2453 225
        $oid = spl_object_hash($coll);
2454 225
        if (isset($this->collectionUpdates[$oid])) {
2455 215
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2456 215
            unset($this->collectionUpdates[$oid]);
2457 215
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2458 215
        }
2459 225
    }
2460
2461
    /**
2462
     * Checks whether a PersistentCollection is scheduled for update.
2463
     *
2464
     * @param PersistentCollectionInterface $coll
2465
     * @return boolean
2466
     */
2467 132
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2468
    {
2469 132
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2470
    }
2471
2472
    /**
2473
     * INTERNAL:
2474
     * Gets PersistentCollections that have been visited during computing change
2475
     * set of $document
2476
     *
2477
     * @param object $document
2478
     * @return PersistentCollectionInterface[]
2479
     */
2480 596
    public function getVisitedCollections($document)
2481
    {
2482 596
        $oid = spl_object_hash($document);
2483 596
        return isset($this->visitedCollections[$oid])
2484 596
                ? $this->visitedCollections[$oid]
2485 596
                : array();
2486
    }
2487
2488
    /**
2489
     * INTERNAL:
2490
     * Gets PersistentCollections that are scheduled to update and related to $document
2491
     *
2492
     * @param object $document
2493
     * @return array
2494
     */
2495 596
    public function getScheduledCollections($document)
2496
    {
2497 596
        $oid = spl_object_hash($document);
2498 596
        return isset($this->hasScheduledCollections[$oid])
2499 596
                ? $this->hasScheduledCollections[$oid]
2500 596
                : array();
2501
    }
2502
2503
    /**
2504
     * Checks whether the document is related to a PersistentCollection
2505
     * scheduled for update or deletion.
2506
     *
2507
     * @param object $document
2508
     * @return boolean
2509
     */
2510 52
    public function hasScheduledCollections($document)
2511
    {
2512 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2513
    }
2514
2515
    /**
2516
     * Marks the PersistentCollection's top-level owner as having a relation to
2517
     * a collection scheduled for update or deletion.
2518
     *
2519
     * If the owner is not scheduled for any lifecycle action, it will be
2520
     * scheduled for update to ensure that versioning takes place if necessary.
2521
     *
2522
     * If the collection is nested within atomic collection, it is immediately
2523
     * unscheduled and atomic one is scheduled for update instead. This makes
2524
     * calculating update data way easier.
2525
     *
2526
     * @param PersistentCollectionInterface $coll
2527
     */
2528 248
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2529
    {
2530 248
        $document = $this->getOwningDocument($coll->getOwner());
2531 248
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2532
2533 248
        if ($document !== $coll->getOwner()) {
2534 25
            $parent = $coll->getOwner();
2535 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2536 25
                list($mapping, $parent, ) = $parentAssoc;
2537 25
            }
2538 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2539 8
                $class = $this->dm->getClassMetadata(get_class($document));
2540 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...
2541 8
                $this->scheduleCollectionUpdate($atomicCollection);
2542 8
                $this->unscheduleCollectionDeletion($coll);
2543 8
                $this->unscheduleCollectionUpdate($coll);
2544 8
            }
2545 25
        }
2546
2547 248
        if ( ! $this->isDocumentScheduled($document)) {
2548 50
            $this->scheduleForUpdate($document);
2549 50
        }
2550 248
    }
2551
2552
    /**
2553
     * Get the top-most owning document of a given document
2554
     *
2555
     * If a top-level document is provided, that same document will be returned.
2556
     * For an embedded document, we will walk through parent associations until
2557
     * we find a top-level document.
2558
     *
2559
     * @param object $document
2560
     * @throws \UnexpectedValueException when a top-level document could not be found
2561
     * @return object
2562
     */
2563 250
    public function getOwningDocument($document)
2564
    {
2565 250
        $class = $this->dm->getClassMetadata(get_class($document));
2566 250
        while ($class->isEmbeddedDocument) {
2567 40
            $parentAssociation = $this->getParentAssociation($document);
2568
2569 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...
2570
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2571
            }
2572
2573 40
            list(, $document, ) = $parentAssociation;
2574 40
            $class = $this->dm->getClassMetadata(get_class($document));
2575 40
        }
2576
2577 250
        return $document;
2578
    }
2579
2580
    /**
2581
     * Gets the class name for an association (embed or reference) with respect
2582
     * to any discriminator value.
2583
     *
2584
     * @param array      $mapping Field mapping for the association
2585
     * @param array|null $data    Data for the embedded document or reference
2586
     * @return string Class name.
2587
     */
2588 222
    public function getClassNameForAssociation(array $mapping, $data)
2589
    {
2590 222
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2591
2592 222
        $discriminatorValue = null;
2593 222
        if (isset($discriminatorField, $data[$discriminatorField])) {
2594 21
            $discriminatorValue = $data[$discriminatorField];
2595 222
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2596
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2597
        }
2598
2599 222
        if ($discriminatorValue !== null) {
2600 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2601 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2602 21
                : $discriminatorValue;
2603
        }
2604
2605 202
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2606
2607 202 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...
2608 15
            $discriminatorValue = $data[$class->discriminatorField];
2609 202
        } elseif ($class->defaultDiscriminatorValue !== null) {
2610 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2611 1
        }
2612
2613 202
        if ($discriminatorValue !== null) {
2614 16
            return isset($class->discriminatorMap[$discriminatorValue])
2615 16
                ? $class->discriminatorMap[$discriminatorValue]
2616 16
                : $discriminatorValue;
2617
        }
2618
2619 186
        return $mapping['targetDocument'];
2620
    }
2621
2622
    /**
2623
     * INTERNAL:
2624
     * Creates a document. Used for reconstitution of documents during hydration.
2625
     *
2626
     * @ignore
2627
     * @param string $className The name of the document class.
2628
     * @param array $data The data for the document.
2629
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2630
     * @param object $document The document to be hydrated into in case of creation
2631
     * @return object The document instance.
2632
     * @internal Highly performance-sensitive method.
2633
     */
2634 416
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2635
    {
2636 416
        $class = $this->dm->getClassMetadata($className);
2637
2638
        // @TODO figure out how to remove this
2639 416
        $discriminatorValue = null;
2640 416 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...
2641 19
            $discriminatorValue = $data[$class->discriminatorField];
2642 416
        } elseif (isset($class->defaultDiscriminatorValue)) {
2643 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2644 2
        }
2645
2646 416
        if ($discriminatorValue !== null) {
2647 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2648 20
                ? $class->discriminatorMap[$discriminatorValue]
2649 20
                : $discriminatorValue;
2650
2651 20
            $class = $this->dm->getClassMetadata($className);
2652
2653 20
            unset($data[$class->discriminatorField]);
2654 20
        }
2655
        
2656 416
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2657 2
            $document = $class->newInstance();
2658 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2659 2
            return $document;
2660
        }
2661
2662 415
        $isManagedObject = false;
2663 415
        if (! $class->isQueryResultDocument) {
2664 415
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2665 415
            $serializedId = serialize($id);
2666 415
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2667 415
        }
2668
2669 415
        if ($isManagedObject) {
2670 107
            $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...
2671 107
            $oid = spl_object_hash($document);
2672 107
            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...
2673 12
                $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...
2674 12
                $overrideLocalValues = true;
2675 12
                if ($document instanceof NotifyPropertyChanged) {
2676
                    $document->addPropertyChangedListener($this);
2677
                }
2678 12
            } else {
2679 103
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2680
            }
2681 107
            if ($overrideLocalValues) {
2682 50
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2683 50
                $this->originalDocumentData[$oid] = $data;
2684 50
            }
2685 107
        } else {
2686 374
            if ($document === null) {
2687 374
                $document = $class->newInstance();
2688 374
            }
2689
2690 374
            if (! $class->isQueryResultDocument) {
2691 373
                $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...
2692 373
                $oid = spl_object_hash($document);
2693 373
                $this->documentStates[$oid] = self::STATE_MANAGED;
2694 373
                $this->identityMap[$class->name][$serializedId] = $document;
2695 373
            }
2696
2697 374
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2698
2699 374
            if (! $class->isQueryResultDocument) {
2700 373
                $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...
2701 373
            }
2702
        }
2703
2704 415
        return $document;
2705
    }
2706
2707
    /**
2708
     * Initializes (loads) an uninitialized persistent collection of a document.
2709
     *
2710
     * @param PersistentCollectionInterface $collection The collection to initialize.
2711
     */
2712 169
    public function loadCollection(PersistentCollectionInterface $collection)
2713
    {
2714 169
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2715 169
        $this->lifecycleEventManager->postCollectionLoad($collection);
2716 169
    }
2717
2718
    /**
2719
     * Gets the identity map of the UnitOfWork.
2720
     *
2721
     * @return array
2722
     */
2723
    public function getIdentityMap()
2724
    {
2725
        return $this->identityMap;
2726
    }
2727
2728
    /**
2729
     * Gets the original data of a document. The original data is the data that was
2730
     * present at the time the document was reconstituted from the database.
2731
     *
2732
     * @param object $document
2733
     * @return array
2734
     */
2735 1
    public function getOriginalDocumentData($document)
2736
    {
2737 1
        $oid = spl_object_hash($document);
2738 1
        if (isset($this->originalDocumentData[$oid])) {
2739 1
            return $this->originalDocumentData[$oid];
2740
        }
2741
        return array();
2742
    }
2743
2744
    /**
2745
     * @ignore
2746
     */
2747 55
    public function setOriginalDocumentData($document, array $data)
2748
    {
2749 55
        $oid = spl_object_hash($document);
2750 55
        $this->originalDocumentData[$oid] = $data;
2751 55
        unset($this->documentChangeSets[$oid]);
2752 55
    }
2753
2754
    /**
2755
     * INTERNAL:
2756
     * Sets a property value of the original data array of a document.
2757
     *
2758
     * @ignore
2759
     * @param string $oid
2760
     * @param string $property
2761
     * @param mixed $value
2762
     */
2763 4
    public function setOriginalDocumentProperty($oid, $property, $value)
2764
    {
2765 4
        $this->originalDocumentData[$oid][$property] = $value;
2766 4
    }
2767
2768
    /**
2769
     * Gets the identifier of a document.
2770
     *
2771
     * @param object $document
2772
     * @return mixed The identifier value
2773
     */
2774 435
    public function getDocumentIdentifier($document)
2775
    {
2776 435
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2777 435
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2778
    }
2779
2780
    /**
2781
     * Checks whether the UnitOfWork has any pending insertions.
2782
     *
2783
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2784
     */
2785
    public function hasPendingInsertions()
2786
    {
2787
        return ! empty($this->documentInsertions);
2788
    }
2789
2790
    /**
2791
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2792
     * number of documents in the identity map.
2793
     *
2794
     * @return integer
2795
     */
2796 2
    public function size()
2797
    {
2798 2
        $count = 0;
2799 2
        foreach ($this->identityMap as $documentSet) {
2800 2
            $count += count($documentSet);
2801 2
        }
2802 2
        return $count;
2803
    }
2804
2805
    /**
2806
     * INTERNAL:
2807
     * Registers a document as managed.
2808
     *
2809
     * TODO: This method assumes that $id is a valid PHP identifier for the
2810
     * document class. If the class expects its database identifier to be a
2811
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2812
     * document identifiers map will become inconsistent with the identity map.
2813
     * In the future, we may want to round-trip $id through a PHP and database
2814
     * conversion and throw an exception if it's inconsistent.
2815
     *
2816
     * @param object $document The document.
2817
     * @param array $id The identifier values.
2818
     * @param array $data The original document data.
2819
     */
2820 396
    public function registerManaged($document, $id, array $data)
2821
    {
2822 396
        $oid = spl_object_hash($document);
2823 396
        $class = $this->dm->getClassMetadata(get_class($document));
2824
2825 396
        if ( ! $class->identifier || $id === null) {
2826 106
            $this->documentIdentifiers[$oid] = $oid;
2827 106
        } else {
2828 390
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2829
        }
2830
2831 396
        $this->documentStates[$oid] = self::STATE_MANAGED;
2832 396
        $this->originalDocumentData[$oid] = $data;
2833 396
        $this->addToIdentityMap($document);
2834 396
    }
2835
2836
    /**
2837
     * INTERNAL:
2838
     * Clears the property changeset of the document with the given OID.
2839
     *
2840
     * @param string $oid The document's OID.
2841
     */
2842 1
    public function clearDocumentChangeSet($oid)
2843
    {
2844 1
        $this->documentChangeSets[$oid] = array();
2845 1
    }
2846
2847
    /* PropertyChangedListener implementation */
2848
2849
    /**
2850
     * Notifies this UnitOfWork of a property change in a document.
2851
     *
2852
     * @param object $document The document that owns the property.
2853
     * @param string $propertyName The name of the property that changed.
2854
     * @param mixed $oldValue The old value of the property.
2855
     * @param mixed $newValue The new value of the property.
2856
     */
2857 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2858
    {
2859 2
        $oid = spl_object_hash($document);
2860 2
        $class = $this->dm->getClassMetadata(get_class($document));
2861
2862 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2863 1
            return; // ignore non-persistent fields
2864
        }
2865
2866
        // Update changeset and mark document for synchronization
2867 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2868 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2869 2
            $this->scheduleForDirtyCheck($document);
2870 2
        }
2871 2
    }
2872
2873
    /**
2874
     * Gets the currently scheduled document insertions in this UnitOfWork.
2875
     *
2876
     * @return array
2877
     */
2878 5
    public function getScheduledDocumentInsertions()
2879
    {
2880 5
        return $this->documentInsertions;
2881
    }
2882
2883
    /**
2884
     * Gets the currently scheduled document upserts in this UnitOfWork.
2885
     *
2886
     * @return array
2887
     */
2888 3
    public function getScheduledDocumentUpserts()
2889
    {
2890 3
        return $this->documentUpserts;
2891
    }
2892
2893
    /**
2894
     * Gets the currently scheduled document updates in this UnitOfWork.
2895
     *
2896
     * @return array
2897
     */
2898 3
    public function getScheduledDocumentUpdates()
2899
    {
2900 3
        return $this->documentUpdates;
2901
    }
2902
2903
    /**
2904
     * Gets the currently scheduled document deletions in this UnitOfWork.
2905
     *
2906
     * @return array
2907
     */
2908
    public function getScheduledDocumentDeletions()
2909
    {
2910
        return $this->documentDeletions;
2911
    }
2912
2913
    /**
2914
     * Get the currently scheduled complete collection deletions
2915
     *
2916
     * @return array
2917
     */
2918
    public function getScheduledCollectionDeletions()
2919
    {
2920
        return $this->collectionDeletions;
2921
    }
2922
2923
    /**
2924
     * Gets the currently scheduled collection inserts, updates and deletes.
2925
     *
2926
     * @return array
2927
     */
2928
    public function getScheduledCollectionUpdates()
2929
    {
2930
        return $this->collectionUpdates;
2931
    }
2932
2933
    /**
2934
     * Helper method to initialize a lazy loading proxy or persistent collection.
2935
     *
2936
     * @param object
2937
     * @return void
2938
     */
2939
    public function initializeObject($obj)
2940
    {
2941
        if ($obj instanceof Proxy) {
2942
            $obj->__load();
2943
        } elseif ($obj instanceof PersistentCollectionInterface) {
2944
            $obj->initialize();
2945
        }
2946
    }
2947
2948 1
    private function objToStr($obj)
2949
    {
2950 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2951
    }
2952
}
2953