Completed
Push — master ( 785c50...2c0a7f )
by Maciej
16s
created

UnitOfWork::getOrCreateDocument()   D

Complexity

Conditions 15
Paths 261

Size

Total Lines 72
Code Lines 47

Duplication

Lines 5
Ratio 6.94 %

Code Coverage

Tests 43
CRAP Score 15.0196

Importance

Changes 0
Metric Value
dl 5
loc 72
ccs 43
cts 45
cp 0.9556
rs 4.1144
c 0
b 0
f 0
cc 15
eloc 47
nc 261
nop 4
crap 15.0196

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * 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 1094
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
271
    {
272 1094
        $this->dm = $dm;
273 1094
        $this->evm = $evm;
274 1094
        $this->hydratorFactory = $hydratorFactory;
275 1094
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
276 1094
    }
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 759
    public function getPersistenceBuilder()
285
    {
286 759
        if ( ! $this->persistenceBuilder) {
287 759
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
288
        }
289 759
        return $this->persistenceBuilder;
290
    }
291
292
    /**
293
     * Sets the parent association for a given embedded document.
294
     *
295
     * @param object $document
296
     * @param array $mapping
297
     * @param object $parent
298
     * @param string $propertyPath
299
     */
300 205
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
301
    {
302 205
        $oid = spl_object_hash($document);
303 205
        $this->embeddedDocumentsRegistry[$oid] = $document;
304 205
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
305 205
    }
306
307
    /**
308
     * Gets the parent association for a given embedded document.
309
     *
310
     *     <code>
311
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
312
     *     </code>
313
     *
314
     * @param object $document
315
     * @return array $association
316
     */
317 233
    public function getParentAssociation($document)
318
    {
319 233
        $oid = spl_object_hash($document);
320 233
        if ( ! isset($this->parentAssociations[$oid])) {
321 227
            return null;
322
        }
323 181
        return $this->parentAssociations[$oid];
324
    }
325
326
    /**
327
     * Get the document persister instance for the given document name
328
     *
329
     * @param string $documentName
330
     * @return Persisters\DocumentPersister
331
     */
332 757
    public function getDocumentPersister($documentName)
333
    {
334 757
        if ( ! isset($this->persisters[$documentName])) {
335 743
            $class = $this->dm->getClassMetadata($documentName);
336 743
            $pb = $this->getPersistenceBuilder();
337 743
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
338
        }
339 757
        return $this->persisters[$documentName];
340
    }
341
342
    /**
343
     * Get the collection persister instance.
344
     *
345
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
346
     */
347 757
    public function getCollectionPersister()
348
    {
349 757
        if ( ! isset($this->collectionPersister)) {
350 757
            $pb = $this->getPersistenceBuilder();
351 757
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
352
        }
353 757
        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 623
    public function commit($document = null, array $options = array())
382
    {
383
        // Raise preFlush
384 623
        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 623
        if ($document === null) {
390 617
            $this->computeChangeSets();
391 14
        } elseif (is_object($document)) {
392 13
            $this->computeSingleDocumentChangeSet($document);
393 1
        } elseif (is_array($document)) {
394 1
            foreach ($document as $object) {
395 1
                $this->computeSingleDocumentChangeSet($object);
396
            }
397
        }
398
399 621
        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 259
            $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 216
            $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 202
            $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
        ) {
407 25
            return; // Nothing to do.
408
        }
409
410 618
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
411 50
            foreach ($this->orphanRemovals as $removal) {
412 50
                $this->remove($removal);
413
            }
414
        }
415
416
        // Raise onFlush
417 618
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419
        }
420
421 618
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 88
            list($class, $documents) = $classAndDocuments;
423 88
            $this->executeUpserts($class, $documents, $options);
424
        }
425
426 618
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 541
            list($class, $documents) = $classAndDocuments;
428 541
            $this->executeInserts($class, $documents, $options);
429
        }
430
431 617
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 232
            list($class, $documents) = $classAndDocuments;
433 232
            $this->executeUpdates($class, $documents, $options);
434
        }
435
436 617
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 71
            list($class, $documents) = $classAndDocuments;
438 71
            $this->executeDeletions($class, $documents, $options);
439
        }
440
441
        // Raise postFlush
442 617
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 617
        $this->documentInsertions =
448 617
        $this->documentUpserts =
449 617
        $this->documentUpdates =
450 617
        $this->documentDeletions =
451 617
        $this->documentChangeSets =
452 617
        $this->collectionUpdates =
453 617
        $this->collectionDeletions =
454 617
        $this->visitedCollections =
455 617
        $this->scheduledForDirtyCheck =
456 617
        $this->orphanRemovals =
457 617
        $this->hasScheduledCollections = array();
458 617
    }
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 618
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 618
        if (empty($documents)) {
470 618
            return array();
471
        }
472 617
        $divided = array();
473 617
        $embeds = array();
474 617
        foreach ($documents as $oid => $d) {
475 617
            $className = get_class($d);
476 617
            if (isset($embeds[$className])) {
477 78
                continue;
478
            }
479 617
            if (isset($divided[$className])) {
480 160
                $divided[$className][1][$oid] = $d;
481 160
                continue;
482
            }
483 617
            $class = $this->dm->getClassMetadata($className);
484 617
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 183
                $embeds[$className] = true;
486 183
                continue;
487
            }
488 617
            if (empty($divided[$class->name])) {
489 617
                $divided[$class->name] = array($class, array($oid => $d));
490
            } else {
491 4
                $divided[$class->name][1][$oid] = $d;
492
            }
493
        }
494 617
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 625 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 625
        foreach ($this->documentInsertions as $document) {
505 552
            $class = $this->dm->getClassMetadata(get_class($document));
506 552
            if ( ! $class->isEmbeddedDocument) {
507 546
                $this->computeChangeSet($class, $document);
508
            }
509
        }
510 624
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 624 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 624
        foreach ($this->documentUpserts as $document) {
520 87
            $class = $this->dm->getClassMetadata(get_class($document));
521 87
            if ( ! $class->isEmbeddedDocument) {
522 87
                $this->computeChangeSet($class, $document);
523
            }
524
        }
525 624
    }
526
527
    /**
528
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
529
     *
530
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
531
     * 2. Proxies are skipped.
532
     * 3. Only if document is properly managed.
533
     *
534
     * @param  object $document
535
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
536
     * @return void
537
     */
538 14
    private function computeSingleDocumentChangeSet($document)
539
    {
540 14
        $state = $this->getDocumentState($document);
541
542 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
543 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
544
        }
545
546 13
        $class = $this->dm->getClassMetadata(get_class($document));
547
548 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
549 10
            $this->persist($document);
550
        }
551
552
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
553 13
        $this->computeScheduleInsertsChangeSets();
554 13
        $this->computeScheduleUpsertsChangeSets();
555
556
        // Ignore uninitialized proxy objects
557 13
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
558
            return;
559
        }
560
561
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
562 13
        $oid = spl_object_hash($document);
563
564 13 View Code Duplication
        if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
565 10
            && ! isset($this->documentUpserts[$oid])
566 9
            && ! isset($this->documentDeletions[$oid])
567 8
            && isset($this->documentStates[$oid])
568
        ) {
569 8
            $this->computeChangeSet($class, $document);
570
        }
571 13
    }
572
573
    /**
574
     * Gets the changeset for a document.
575
     *
576
     * @param object $document
577
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
578
     */
579 618
    public function getDocumentChangeSet($document)
580
    {
581 618
        $oid = spl_object_hash($document);
582 618
        if (isset($this->documentChangeSets[$oid])) {
583 615
            return $this->documentChangeSets[$oid];
584
        }
585 60
        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 625
    public function getDocumentActualData($document)
607
    {
608 625
        $class = $this->dm->getClassMetadata(get_class($document));
609 625
        $actualData = array();
610 625
        foreach ($class->reflFields as $name => $refProp) {
611 625
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 625
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 52
                continue;
615
            }
616 625
            $value = $refProp->getValue($document);
617 625
            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 625
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 422
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 408
                if ( ! $value instanceof Collection) {
625 144
                    $value = new ArrayCollection($value);
626
                }
627
628
                // Inject PersistentCollection
629 408
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
630 408
                $coll->setOwner($document, $mapping);
631 408
                $coll->setDirty( ! $value->isEmpty());
632 408
                $class->reflFields[$name]->setValue($document, $coll);
633 408
                $actualData[$name] = $coll;
634
            } else {
635 625
                $actualData[$name] = $value;
636
            }
637
        }
638 625
        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 622
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 622
        if ( ! $class->isInheritanceTypeNone()) {
668 192
            $class = $this->dm->getClassMetadata(get_class($document));
669
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 622 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
673 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
674
        }
675
676 622
        $this->computeOrRecomputeChangeSet($class, $document);
677 621
    }
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 622
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 622
        $oid = spl_object_hash($document);
689 622
        $actualData = $this->getDocumentActualData($document);
690 622
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 622
        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 622
            $this->originalDocumentData[$oid] = $actualData;
695 622
            $changeSet = array();
696 622
            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 622
                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 622 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1872
                            // Do not merge fields marked lazy that have not been fetched
1873 1
                            continue;
1874 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1875
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1876
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1877
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1878
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1879
                                $relatedId = $targetClass->getIdentifierObject($other);
1880
1881
                                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...
1882
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1883
                                } else {
1884
                                    $other = $this
1885
                                        ->dm
1886
                                        ->getProxyFactory()
1887
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1888
                                    $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...
1889
                                }
1890
                            }
1891
1892
                            $prop->setValue($managedCopy, $other);
1893
                        }
1894
                    } else {
1895 12
                        $mergeCol = $prop->getValue($document);
1896
1897 12
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1898
                            /* Do not merge fields marked lazy that have not
1899
                             * been fetched. Keep the lazy persistent collection
1900
                             * of the managed copy.
1901
                             */
1902 3
                            continue;
1903
                        }
1904
1905 12
                        $managedCol = $prop->getValue($managedCopy);
1906
1907 12
                        if ( ! $managedCol) {
1908 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1909 3
                            $managedCol->setOwner($managedCopy, $assoc2);
1910 3
                            $prop->setValue($managedCopy, $managedCol);
1911 3
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1912
                        }
1913
1914
                        /* Note: do not process association's target documents.
1915
                         * They will be handled during the cascade. Initialize
1916
                         * and, if necessary, clear $managedCol for now.
1917
                         */
1918 12
                        if ($assoc2['isCascadeMerge']) {
1919 12
                            $managedCol->initialize();
1920
1921
                            // If $managedCol differs from the merged collection, clear and set dirty
1922 12
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1923 3
                                $managedCol->unwrap()->clear();
1924 3
                                $managedCol->setDirty(true);
1925
1926 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1927
                                    $this->scheduleForDirtyCheck($managedCopy);
1928
                                }
1929
                            }
1930
                        }
1931
                    }
1932
                }
1933
1934 15
                if ($class->isChangeTrackingNotify()) {
1935
                    // Just treat all properties as changed, there is no other choice.
1936
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1937
                }
1938
            }
1939
1940 15
            if ($class->isChangeTrackingDeferredExplicit()) {
1941
                $this->scheduleForDirtyCheck($document);
1942
            }
1943
        }
1944
1945 15
        if ($prevManagedCopy !== null) {
1946 8
            $assocField = $assoc['fieldName'];
1947 8
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1948
1949 8
            if ($assoc['type'] === 'one') {
1950 4
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1951
            } else {
1952 6
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1953
1954 6
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1955 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1956
                }
1957
            }
1958
        }
1959
1960
        // Mark the managed copy visited as well
1961 15
        $visited[spl_object_hash($managedCopy)] = true;
1962
1963 15
        $this->cascadeMerge($document, $managedCopy, $visited);
1964
1965 15
        return $managedCopy;
1966
    }
1967
1968
    /**
1969
     * Detaches a document from the persistence management. It's persistence will
1970
     * no longer be managed by Doctrine.
1971
     *
1972
     * @param object $document The document to detach.
1973
     */
1974 12
    public function detach($document)
1975
    {
1976 12
        $visited = array();
1977 12
        $this->doDetach($document, $visited);
1978 12
    }
1979
1980
    /**
1981
     * Executes a detach operation on the given document.
1982
     *
1983
     * @param object $document
1984
     * @param array $visited
1985
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1986
     */
1987 17
    private function doDetach($document, array &$visited)
1988
    {
1989 17
        $oid = spl_object_hash($document);
1990 17
        if (isset($visited[$oid])) {
1991 4
            return; // Prevent infinite recursion
1992
        }
1993
1994 17
        $visited[$oid] = $document; // mark visited
1995
1996 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1997 17
            case self::STATE_MANAGED:
1998 17
                $this->removeFromIdentityMap($document);
1999 17
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2000 17
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2001 17
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2002 17
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2003 17
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2004 17
                break;
2005 4
            case self::STATE_NEW:
2006 4
            case self::STATE_DETACHED:
2007 4
                return;
2008
        }
2009
2010 17
        $this->cascadeDetach($document, $visited);
2011 17
    }
2012
2013
    /**
2014
     * Refreshes the state of the given document from the database, overwriting
2015
     * any local, unpersisted changes.
2016
     *
2017
     * @param object $document The document to refresh.
2018
     * @throws \InvalidArgumentException If the document is not MANAGED.
2019
     */
2020 22
    public function refresh($document)
2021
    {
2022 22
        $visited = array();
2023 22
        $this->doRefresh($document, $visited);
2024 21
    }
2025
2026
    /**
2027
     * Executes a refresh operation on a document.
2028
     *
2029
     * @param object $document The document to refresh.
2030
     * @param array $visited The already visited documents during cascades.
2031
     * @throws \InvalidArgumentException If the document is not MANAGED.
2032
     */
2033 22
    private function doRefresh($document, array &$visited)
2034
    {
2035 22
        $oid = spl_object_hash($document);
2036 22
        if (isset($visited[$oid])) {
2037
            return; // Prevent infinite recursion
2038
        }
2039
2040 22
        $visited[$oid] = $document; // mark visited
2041
2042 22
        $class = $this->dm->getClassMetadata(get_class($document));
2043
2044 22
        if ( ! $class->isEmbeddedDocument) {
2045 22
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2046 21
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2047 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...
2048
            } else {
2049 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2050
            }
2051
        }
2052
2053 21
        $this->cascadeRefresh($document, $visited);
2054 21
    }
2055
2056
    /**
2057
     * Cascades a refresh operation to associated documents.
2058
     *
2059
     * @param object $document
2060
     * @param array $visited
2061
     */
2062 21
    private function cascadeRefresh($document, array &$visited)
2063
    {
2064 21
        $class = $this->dm->getClassMetadata(get_class($document));
2065
2066 21
        $associationMappings = array_filter(
2067 21
            $class->associationMappings,
2068
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2069
        );
2070
2071 21
        foreach ($associationMappings as $mapping) {
2072 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2073 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2074 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2075
                    // Unwrap so that foreach() does not initialize
2076 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2077
                }
2078 15
                foreach ($relatedDocuments as $relatedDocument) {
2079 15
                    $this->doRefresh($relatedDocument, $visited);
2080
                }
2081 10
            } elseif ($relatedDocuments !== null) {
2082 2
                $this->doRefresh($relatedDocuments, $visited);
2083
            }
2084
        }
2085 21
    }
2086
2087
    /**
2088
     * Cascades a detach operation to associated documents.
2089
     *
2090
     * @param object $document
2091
     * @param array $visited
2092
     */
2093 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...
2094
    {
2095 17
        $class = $this->dm->getClassMetadata(get_class($document));
2096 17
        foreach ($class->fieldMappings as $mapping) {
2097 17
            if ( ! $mapping['isCascadeDetach']) {
2098 17
                continue;
2099
            }
2100 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2101 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2102 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2103
                    // Unwrap so that foreach() does not initialize
2104 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2105
                }
2106 11
                foreach ($relatedDocuments as $relatedDocument) {
2107 11
                    $this->doDetach($relatedDocument, $visited);
2108
                }
2109 11
            } elseif ($relatedDocuments !== null) {
2110 9
                $this->doDetach($relatedDocuments, $visited);
2111
            }
2112
        }
2113 17
    }
2114
    /**
2115
     * Cascades a merge operation to associated documents.
2116
     *
2117
     * @param object $document
2118
     * @param object $managedCopy
2119
     * @param array $visited
2120
     */
2121 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2122
    {
2123 15
        $class = $this->dm->getClassMetadata(get_class($document));
2124
2125 15
        $associationMappings = array_filter(
2126 15
            $class->associationMappings,
2127
            function ($assoc) { return $assoc['isCascadeMerge']; }
2128
        );
2129
2130 15
        foreach ($associationMappings as $assoc) {
2131 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2132
2133 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2134 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2135
                    // Collections are the same, so there is nothing to do
2136 1
                    continue;
2137
                }
2138
2139 10
                foreach ($relatedDocuments as $relatedDocument) {
2140 10
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2141
                }
2142 7
            } elseif ($relatedDocuments !== null) {
2143 5
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2144
            }
2145
        }
2146 15
    }
2147
2148
    /**
2149
     * Cascades the save operation to associated documents.
2150
     *
2151
     * @param object $document
2152
     * @param array $visited
2153
     */
2154 651
    private function cascadePersist($document, array &$visited)
2155
    {
2156 651
        $class = $this->dm->getClassMetadata(get_class($document));
2157
2158 651
        $associationMappings = array_filter(
2159 651
            $class->associationMappings,
2160
            function ($assoc) { return $assoc['isCascadePersist']; }
2161
        );
2162
2163 651
        foreach ($associationMappings as $fieldName => $mapping) {
2164 456
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2165
2166 456
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2167 378
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2168 17
                    if ($relatedDocuments->getOwner() !== $document) {
2169 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2170
                    }
2171
                    // Unwrap so that foreach() does not initialize
2172 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2173
                }
2174
2175 378
                $count = 0;
2176 378
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2177 206
                    if ( ! empty($mapping['embedded'])) {
2178 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2179 126
                        if ($knownParent && $knownParent !== $document) {
2180 4
                            $relatedDocument = clone $relatedDocument;
2181 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2182
                        }
2183 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2184 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2185
                    }
2186 378
                    $this->doPersist($relatedDocument, $visited);
2187
                }
2188 364
            } elseif ($relatedDocuments !== null) {
2189 138
                if ( ! empty($mapping['embedded'])) {
2190 77
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2191 77
                    if ($knownParent && $knownParent !== $document) {
2192 6
                        $relatedDocuments = clone $relatedDocuments;
2193 6
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2194
                    }
2195 77
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2196
                }
2197 138
                $this->doPersist($relatedDocuments, $visited);
2198
            }
2199
        }
2200 649
    }
2201
2202
    /**
2203
     * Cascades the delete operation to associated documents.
2204
     *
2205
     * @param object $document
2206
     * @param array $visited
2207
     */
2208 75 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...
2209
    {
2210 75
        $class = $this->dm->getClassMetadata(get_class($document));
2211 75
        foreach ($class->fieldMappings as $mapping) {
2212 75
            if ( ! $mapping['isCascadeRemove']) {
2213 74
                continue;
2214
            }
2215 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...
2216 2
                $document->__load();
2217
            }
2218
2219 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2220 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2221
                // If its a PersistentCollection initialization is intended! No unwrap!
2222 25
                foreach ($relatedDocuments as $relatedDocument) {
2223 25
                    $this->doRemove($relatedDocument, $visited);
2224
                }
2225 24
            } elseif ($relatedDocuments !== null) {
2226 13
                $this->doRemove($relatedDocuments, $visited);
2227
            }
2228
        }
2229 75
    }
2230
2231
    /**
2232
     * Acquire a lock on the given document.
2233
     *
2234
     * @param object $document
2235
     * @param int $lockMode
2236
     * @param int $lockVersion
2237
     * @throws LockException
2238
     * @throws \InvalidArgumentException
2239
     */
2240 9
    public function lock($document, $lockMode, $lockVersion = null)
2241
    {
2242 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2243 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2244
        }
2245
2246 8
        $documentName = get_class($document);
2247 8
        $class = $this->dm->getClassMetadata($documentName);
2248
2249 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2250 3
            if ( ! $class->isVersioned) {
2251 1
                throw LockException::notVersioned($documentName);
2252
            }
2253
2254 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...
2255 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2256 2
                if ($documentVersion != $lockVersion) {
2257 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2258
                }
2259
            }
2260 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2261 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2262
        }
2263 6
    }
2264
2265
    /**
2266
     * Releases a lock on the given document.
2267
     *
2268
     * @param object $document
2269
     * @throws \InvalidArgumentException
2270
     */
2271 1
    public function unlock($document)
2272
    {
2273 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2274
            throw new \InvalidArgumentException("Document is not MANAGED.");
2275
        }
2276 1
        $documentName = get_class($document);
2277 1
        $this->getDocumentPersister($documentName)->unlock($document);
2278 1
    }
2279
2280
    /**
2281
     * Clears the UnitOfWork.
2282
     *
2283
     * @param string|null $documentName if given, only documents of this type will get detached.
2284
     */
2285 418
    public function clear($documentName = null)
2286
    {
2287 418
        if ($documentName === null) {
2288 410
            $this->identityMap =
2289 410
            $this->documentIdentifiers =
2290 410
            $this->originalDocumentData =
2291 410
            $this->documentChangeSets =
2292 410
            $this->documentStates =
2293 410
            $this->scheduledForDirtyCheck =
2294 410
            $this->documentInsertions =
2295 410
            $this->documentUpserts =
2296 410
            $this->documentUpdates =
2297 410
            $this->documentDeletions =
2298 410
            $this->collectionUpdates =
2299 410
            $this->collectionDeletions =
2300 410
            $this->parentAssociations =
2301 410
            $this->embeddedDocumentsRegistry =
2302 410
            $this->orphanRemovals =
2303 410
            $this->hasScheduledCollections = array();
2304
        } else {
2305 8
            $visited = array();
2306 8
            foreach ($this->identityMap as $className => $documents) {
2307 8
                if ($className === $documentName) {
2308 5
                    foreach ($documents as $document) {
2309 5
                        $this->doDetach($document, $visited);
2310
                    }
2311
                }
2312
            }
2313
        }
2314
2315 418 View Code Duplication
        if ($this->evm->hasListeners(Events::onClear)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2316
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2317
        }
2318 418
    }
2319
2320
    /**
2321
     * INTERNAL:
2322
     * Schedules an embedded document for removal. The remove() operation will be
2323
     * invoked on that document at the beginning of the next commit of this
2324
     * UnitOfWork.
2325
     *
2326
     * @ignore
2327
     * @param object $document
2328
     */
2329 53
    public function scheduleOrphanRemoval($document)
2330
    {
2331 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2332 53
    }
2333
2334
    /**
2335
     * INTERNAL:
2336
     * Unschedules an embedded or referenced object for removal.
2337
     *
2338
     * @ignore
2339
     * @param object $document
2340
     */
2341 114
    public function unscheduleOrphanRemoval($document)
2342
    {
2343 114
        $oid = spl_object_hash($document);
2344 114
        if (isset($this->orphanRemovals[$oid])) {
2345 1
            unset($this->orphanRemovals[$oid]);
2346
        }
2347 114
    }
2348
2349
    /**
2350
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2351
     *  1) sets owner if it was cloned
2352
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2353
     *  3) NOP if state is OK
2354
     * Returned collection should be used from now on (only important with 2nd point)
2355
     *
2356
     * @param PersistentCollectionInterface $coll
2357
     * @param object $document
2358
     * @param ClassMetadata $class
2359
     * @param string $propName
2360
     * @return PersistentCollectionInterface
2361
     */
2362 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2363
    {
2364 8
        $owner = $coll->getOwner();
2365 8
        if ($owner === null) { // cloned
2366 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2367 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2368 2
            if ( ! $coll->isInitialized()) {
2369 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2370
            }
2371 2
            $newValue = clone $coll;
2372 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2373 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2374 2
            if ($this->isScheduledForUpdate($document)) {
2375
                // @todo following line should be superfluous once collections are stored in change sets
2376
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2377
            }
2378 2
            return $newValue;
2379
        }
2380 6
        return $coll;
2381
    }
2382
2383
    /**
2384
     * INTERNAL:
2385
     * Schedules a complete collection for removal when this UnitOfWork commits.
2386
     *
2387
     * @param PersistentCollectionInterface $coll
2388
     */
2389 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2390
    {
2391 43
        $oid = spl_object_hash($coll);
2392 43
        unset($this->collectionUpdates[$oid]);
2393 43
        if ( ! isset($this->collectionDeletions[$oid])) {
2394 43
            $this->collectionDeletions[$oid] = $coll;
2395 43
            $this->scheduleCollectionOwner($coll);
2396
        }
2397 43
    }
2398
2399
    /**
2400
     * Checks whether a PersistentCollection is scheduled for deletion.
2401
     *
2402
     * @param PersistentCollectionInterface $coll
2403
     * @return boolean
2404
     */
2405 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2406
    {
2407 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2408
    }
2409
2410
    /**
2411
     * INTERNAL:
2412
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2413
     *
2414
     * @param PersistentCollectionInterface $coll
2415
     */
2416 232 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
2455
    {
2456 232
        $oid = spl_object_hash($coll);
2457 232
        if (isset($this->collectionUpdates[$oid])) {
2458 222
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2459 222
            unset($this->collectionUpdates[$oid]);
2460 222
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2461
        }
2462 232
    }
2463
2464
    /**
2465
     * Checks whether a PersistentCollection is scheduled for update.
2466
     *
2467
     * @param PersistentCollectionInterface $coll
2468
     * @return boolean
2469
     */
2470 133
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2471
    {
2472 133
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2473
    }
2474
2475
    /**
2476
     * INTERNAL:
2477
     * Gets PersistentCollections that have been visited during computing change
2478
     * set of $document
2479
     *
2480
     * @param object $document
2481
     * @return PersistentCollectionInterface[]
2482
     */
2483 603
    public function getVisitedCollections($document)
2484
    {
2485 603
        $oid = spl_object_hash($document);
2486 603
        return isset($this->visitedCollections[$oid])
2487 257
                ? $this->visitedCollections[$oid]
2488 603
                : array();
2489
    }
2490
2491
    /**
2492
     * INTERNAL:
2493
     * Gets PersistentCollections that are scheduled to update and related to $document
2494
     *
2495
     * @param object $document
2496
     * @return array
2497
     */
2498 603
    public function getScheduledCollections($document)
2499
    {
2500 603
        $oid = spl_object_hash($document);
2501 603
        return isset($this->hasScheduledCollections[$oid])
2502 255
                ? $this->hasScheduledCollections[$oid]
2503 603
                : array();
2504
    }
2505
2506
    /**
2507
     * Checks whether the document is related to a PersistentCollection
2508
     * scheduled for update or deletion.
2509
     *
2510
     * @param object $document
2511
     * @return boolean
2512
     */
2513 52
    public function hasScheduledCollections($document)
2514
    {
2515 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2516
    }
2517
2518
    /**
2519
     * Marks the PersistentCollection's top-level owner as having a relation to
2520
     * a collection scheduled for update or deletion.
2521
     *
2522
     * If the owner is not scheduled for any lifecycle action, it will be
2523
     * scheduled for update to ensure that versioning takes place if necessary.
2524
     *
2525
     * If the collection is nested within atomic collection, it is immediately
2526
     * unscheduled and atomic one is scheduled for update instead. This makes
2527
     * calculating update data way easier.
2528
     *
2529
     * @param PersistentCollectionInterface $coll
2530
     */
2531 256
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2532
    {
2533 256
        $document = $this->getOwningDocument($coll->getOwner());
2534 256
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2535
2536 256
        if ($document !== $coll->getOwner()) {
2537 25
            $parent = $coll->getOwner();
2538 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2539 25
                list($mapping, $parent, ) = $parentAssoc;
2540
            }
2541 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2542 8
                $class = $this->dm->getClassMetadata(get_class($document));
2543 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...
2544 8
                $this->scheduleCollectionUpdate($atomicCollection);
2545 8
                $this->unscheduleCollectionDeletion($coll);
2546 8
                $this->unscheduleCollectionUpdate($coll);
2547
            }
2548
        }
2549
2550 256
        if ( ! $this->isDocumentScheduled($document)) {
2551 50
            $this->scheduleForUpdate($document);
2552
        }
2553 256
    }
2554
2555
    /**
2556
     * Get the top-most owning document of a given document
2557
     *
2558
     * If a top-level document is provided, that same document will be returned.
2559
     * For an embedded document, we will walk through parent associations until
2560
     * we find a top-level document.
2561
     *
2562
     * @param object $document
2563
     * @throws \UnexpectedValueException when a top-level document could not be found
2564
     * @return object
2565
     */
2566 258
    public function getOwningDocument($document)
2567
    {
2568 258
        $class = $this->dm->getClassMetadata(get_class($document));
2569 258
        while ($class->isEmbeddedDocument) {
2570 40
            $parentAssociation = $this->getParentAssociation($document);
2571
2572 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...
2573
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2574
            }
2575
2576 40
            list(, $document, ) = $parentAssociation;
2577 40
            $class = $this->dm->getClassMetadata(get_class($document));
2578
        }
2579
2580 258
        return $document;
2581
    }
2582
2583
    /**
2584
     * Gets the class name for an association (embed or reference) with respect
2585
     * to any discriminator value.
2586
     *
2587
     * @param array      $mapping Field mapping for the association
2588
     * @param array|null $data    Data for the embedded document or reference
2589
     * @return string Class name.
2590
     */
2591 228
    public function getClassNameForAssociation(array $mapping, $data)
2592
    {
2593 228
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2594
2595 228
        $discriminatorValue = null;
2596 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2597 21
            $discriminatorValue = $data[$discriminatorField];
2598 208
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2599
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2600
        }
2601
2602 228
        if ($discriminatorValue !== null) {
2603 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2604 10
                ? $mapping['discriminatorMap'][$discriminatorValue]
2605 21
                : $discriminatorValue;
2606
        }
2607
2608 208
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2609
2610 208 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2611 15
            $discriminatorValue = $data[$class->discriminatorField];
2612 193
        } elseif ($class->defaultDiscriminatorValue !== null) {
2613 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2614
        }
2615
2616 208
        if ($discriminatorValue !== null) {
2617 16
            return isset($class->discriminatorMap[$discriminatorValue])
2618 14
                ? $class->discriminatorMap[$discriminatorValue]
2619 16
                : $discriminatorValue;
2620
        }
2621
2622 192
        return $mapping['targetDocument'];
2623
    }
2624
2625
    /**
2626
     * INTERNAL:
2627
     * Creates a document. Used for reconstitution of documents during hydration.
2628
     *
2629
     * @ignore
2630
     * @param string $className The name of the document class.
2631
     * @param array $data The data for the document.
2632
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2633
     * @param object $document The document to be hydrated into in case of creation
2634
     * @return object The document instance.
2635
     * @internal Highly performance-sensitive method.
2636
     */
2637 422
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2638
    {
2639 422
        $class = $this->dm->getClassMetadata($className);
2640
2641
        // @TODO figure out how to remove this
2642 422
        $discriminatorValue = null;
2643 422 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2644 19
            $discriminatorValue = $data[$class->discriminatorField];
2645
        } elseif (isset($class->defaultDiscriminatorValue)) {
2646 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2647
        }
2648
2649 422
        if ($discriminatorValue !== null) {
2650 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2651 18
                ? $class->discriminatorMap[$discriminatorValue]
2652 20
                : $discriminatorValue;
2653
2654 20
            $class = $this->dm->getClassMetadata($className);
2655
2656 20
            unset($data[$class->discriminatorField]);
2657
        }
2658
        
2659 422
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2660 2
            $document = $class->newInstance();
2661 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2662 2
            return $document;
2663
        }
2664
2665 421
        $isManagedObject = false;
2666 421
        if (! $class->isQueryResultDocument) {
2667 421
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2668 421
            $serializedId = serialize($id);
2669 421
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2670
        }
2671
2672 421
        if ($isManagedObject) {
2673 110
            $document = $this->identityMap[$class->name][$serializedId];
0 ignored issues
show
Bug introduced by
The variable $serializedId does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2676 14
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2677 14
                $overrideLocalValues = true;
2678 14
                if ($document instanceof NotifyPropertyChanged) {
2679
                    $document->addPropertyChangedListener($this);
2680
                }
2681
            } else {
2682 106
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2683
            }
2684 110
            if ($overrideLocalValues) {
2685 52
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2686 52
                $this->originalDocumentData[$oid] = $data;
2687
            }
2688
        } else {
2689 380
            if ($document === null) {
2690 380
                $document = $class->newInstance();
2691
            }
2692
2693 380
            if (! $class->isQueryResultDocument) {
2694 379
                $this->registerManaged($document, $id, $data);
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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