Completed
Pull Request — master (#1612)
by Maciej
08:32
created

UnitOfWork::getDocumentActualData()   C

Complexity

Conditions 11
Paths 6

Size

Total Lines 34
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 11

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 34
ccs 23
cts 23
cp 1
rs 5.2653
cc 11
eloc 24
nc 6
nop 1
crap 11

How to fix   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 1101
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
271
    {
272 1101
        $this->dm = $dm;
273 1101
        $this->evm = $evm;
274 1101
        $this->hydratorFactory = $hydratorFactory;
275 1101
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
276 1101
    }
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 764
    public function getPersistenceBuilder()
285
    {
286 764
        if ( ! $this->persistenceBuilder) {
287 764
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
288
        }
289 764
        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 762
    public function getDocumentPersister($documentName)
333
    {
334 762
        if ( ! isset($this->persisters[$documentName])) {
335 748
            $class = $this->dm->getClassMetadata($documentName);
336 748
            $pb = $this->getPersistenceBuilder();
337 748
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
338
        }
339 762
        return $this->persisters[$documentName];
340
    }
341
342
    /**
343
     * Get the collection persister instance.
344
     *
345
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
346
     */
347 762
    public function getCollectionPersister()
348
    {
349 762
        if ( ! isset($this->collectionPersister)) {
350 762
            $pb = $this->getPersistenceBuilder();
351 762
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
352
        }
353 762
        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 628
    public function commit($document = null, array $options = array())
382
    {
383
        // Raise preFlush
384 628
        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 628
        if ($document === null) {
390 622
            $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 626
        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 262
            $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 218
            $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 203
            $this->documentUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
565 13
            && ! isset($this->documentUpserts[$oid])
566 13
            && ! isset($this->documentDeletions[$oid])
567 13
            && isset($this->documentStates[$oid])
568
        ) {
569 8
            $this->computeChangeSet($class, $document);
570
        }
571 13
    }
572
573
    /**
574
     * Gets the changeset for a document.
575
     *
576
     * @param object $document
577
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
578
     */
579 623
    public function getDocumentChangeSet($document)
580
    {
581 623
        $oid = spl_object_hash($document);
582 623
        if (isset($this->documentChangeSets[$oid])) {
583 620
            return $this->documentChangeSets[$oid];
584
        }
585 61
        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 630
    public function getDocumentActualData($document)
607
    {
608 630
        $class = $this->dm->getClassMetadata(get_class($document));
609 630
        $actualData = array();
610 630
        foreach ($class->reflFields as $name => $refProp) {
611 630
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 630
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 52
                continue;
615
            }
616 630
            $value = $refProp->getValue($document);
617 630
            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 630
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 630
                && $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 630
                $actualData[$name] = $value;
636
            }
637
        }
638 630
        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 627
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 627
        if ( ! $class->isInheritanceTypeNone()) {
668 192
            $class = $this->dm->getClassMetadata(get_class($document));
669
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 627 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 627
        $this->computeOrRecomputeChangeSet($class, $document);
677 626
    }
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 627
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 627
        $oid = spl_object_hash($document);
689 627
        $actualData = $this->getDocumentActualData($document);
690 627
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 627
        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 627
            $this->originalDocumentData[$oid] = $actualData;
695 627
            $changeSet = array();
696 627
            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 627
                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 627 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 627
                $changeSet[$propName] = array(null, $actualValue);
709
            }
710 627
            $this->documentChangeSets[$oid] = $changeSet;
711
        } else {
712 296
            if ($class->isReadOnly) {
713 2
                return;
714
            }
715
            // Document is "fully" MANAGED: it was already fully persisted before
716
            // and we have a copy of the original data
717 294
            $originalData = $this->originalDocumentData[$oid];
718 294
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
719 294
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
720 2
                $changeSet = $this->documentChangeSets[$oid];
721
            } else {
722 294
                $changeSet = array();
723
            }
724
725 294
            foreach ($actualData as $propName => $actualValue) {
726
                // skip not saved fields
727 294
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
728
                    continue;
729
                }
730
731 294
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
732
733
                // skip if value has not changed
734 294
                if ($orgValue === $actualValue) {
735 293
                    if ($actualValue instanceof PersistentCollectionInterface) {
736 205
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
737
                            // consider dirty collections as changed as well
738 205
                            continue;
739
                        }
740 293
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
741
                        // but consider dirty GridFSFile instances as changed
742 293
                        continue;
743
                    }
744
                }
745
746
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
747 251
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
748 13
                    if ($orgValue !== null) {
749 8
                        $this->scheduleOrphanRemoval($orgValue);
750
                    }
751 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
752 13
                    continue;
753
                }
754
755
                // if owning side of reference-one relationship
756 245
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
757 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
758 1
                        $this->scheduleOrphanRemoval($orgValue);
759
                    }
760
761 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
762 13
                    continue;
763
                }
764
765 238
                if ($isChangeTrackingNotify) {
766 3
                    continue;
767
                }
768
769
                // ignore inverse side of reference relationship
770 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...
771 6
                    continue;
772
                }
773
774
                // Persistent collection was exchanged with the "originally"
775
                // created one. This can only mean it was cloned and replaced
776
                // on another document.
777 234
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
778 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
779
                }
780
781
                // if embed-many or reference-many relationship
782 234
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
783 119
                    $changeSet[$propName] = array($orgValue, $actualValue);
784
                    /* If original collection was exchanged with a non-empty value
785
                     * and $set will be issued, there is no need to $unset it first
786
                     */
787 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
788 28
                        continue;
789
                    }
790 99
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
791 18
                        $this->scheduleCollectionDeletion($orgValue);
792
                    }
793 99
                    continue;
794
                }
795
796
                // skip equivalent date values
797 152
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
798 37
                    $dateType = Type::getType('date');
799 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
800 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
801
802 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
803 30
                        continue;
804
                    }
805
                }
806
807
                // regular field
808 135
                $changeSet[$propName] = array($orgValue, $actualValue);
809
            }
810 294
            if ($changeSet) {
811 240
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
812 21
                    ? $changeSet + $this->documentChangeSets[$oid]
813 235
                    : $changeSet;
814
815 240
                $this->originalDocumentData[$oid] = $actualData;
816 240
                $this->scheduleForUpdate($document);
817
            }
818
        }
819
820
        // Look for changes in associations of the document
821 627
        $associationMappings = array_filter(
822 627
            $class->associationMappings,
823
            function ($assoc) { return empty($assoc['notSaved']); }
824
        );
825
826 627
        foreach ($associationMappings as $mapping) {
827 480
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
828
829 480
            if ($value === null) {
830 325
                continue;
831
            }
832
833 467
            $this->computeAssociationChanges($document, $mapping, $value);
834
835 466
            if (isset($mapping['reference'])) {
836 353
                continue;
837
            }
838
839 363
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
840
841 363
            foreach ($values as $obj) {
842 187
                $oid2 = spl_object_hash($obj);
843
844 187
                if (isset($this->documentChangeSets[$oid2])) {
845 185
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
846
                        // instance of $value is the same as it was previously otherwise there would be
847
                        // change set already in place
848 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
849
                    }
850
851 185
                    if ( ! $isNewDocument) {
852 80
                        $this->scheduleForUpdate($document);
853
                    }
854
855 363
                    break;
856
                }
857
            }
858
        }
859 626
    }
860
861
    /**
862
     * Computes all the changes that have been done to documents and collections
863
     * since the last commit and stores these changes in the _documentChangeSet map
864
     * temporarily for access by the persisters, until the UoW commit is finished.
865
     */
866 625
    public function computeChangeSets()
867
    {
868 625
        $this->computeScheduleInsertsChangeSets();
869 624
        $this->computeScheduleUpsertsChangeSets();
870
871
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
872 624
        foreach ($this->identityMap as $className => $documents) {
873 624
            $class = $this->dm->getClassMetadata($className);
874 624
            if ($class->isEmbeddedDocument) {
875
                /* we do not want to compute changes to embedded documents up front
876
                 * in case embedded document was replaced and its changeset
877
                 * would corrupt data. Embedded documents' change set will
878
                 * be calculated by reachability from owning document.
879
                 */
880 176
                continue;
881
            }
882
883
            // If change tracking is explicit or happens through notification, then only compute
884
            // changes on document of that type that are explicitly marked for synchronization.
885
            switch (true) {
886 624
                case ($class->isChangeTrackingDeferredImplicit()):
887 623
                    $documentsToProcess = $documents;
888 623
                    break;
889
890 4
                case (isset($this->scheduledForDirtyCheck[$className])):
891 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
892 3
                    break;
893
894
                default:
895 4
                    $documentsToProcess = array();
896
897
            }
898
899 624
            foreach ($documentsToProcess as $document) {
900
                // Ignore uninitialized proxy objects
901 620
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
902 10
                    continue;
903
                }
904
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
905 620
                $oid = spl_object_hash($document);
906 620 View Code Duplication
                if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
907 620
                    && ! isset($this->documentUpserts[$oid])
908 620
                    && ! isset($this->documentDeletions[$oid])
909 620
                    && isset($this->documentStates[$oid])
910
                ) {
911 624
                    $this->computeChangeSet($class, $document);
912
                }
913
            }
914
        }
915 624
    }
916
917
    /**
918
     * Computes the changes of an association.
919
     *
920
     * @param object $parentDocument
921
     * @param array $assoc
922
     * @param mixed $value The value of the association.
923
     * @throws \InvalidArgumentException
924
     */
925 467
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
926
    {
927 467
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
928 467
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
929 467
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
930
931 467
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
932 8
            return;
933
        }
934
935 466
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
936 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
937 254
                $this->scheduleCollectionUpdate($value);
938
            }
939 258
            $topmostOwner = $this->getOwningDocument($value->getOwner());
940 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
941 258
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
942 147
                $value->initialize();
943 147
                foreach ($value->getDeletedDocuments() as $orphan) {
944 23
                    $this->scheduleOrphanRemoval($orphan);
945
                }
946
            }
947
        }
948
949
        // Look through the documents, and in any of their associations,
950
        // for transient (new) documents, recursively. ("Persistence by reachability")
951
        // Unwrap. Uninitialized collections will simply be empty.
952 466
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
953
954 466
        $count = 0;
955 466
        foreach ($unwrappedValue as $key => $entry) {
956 371
            if ( ! is_object($entry)) {
957 1
                throw new \InvalidArgumentException(
958 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
959
                );
960
            }
961
962 370
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
963
964 370
            $state = $this->getDocumentState($entry, self::STATE_NEW);
965
966
            // Handle "set" strategy for multi-level hierarchy
967 370
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
968 370
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
969
970 370
            $count++;
971
972
            switch ($state) {
973 370
                case self::STATE_NEW:
974 68
                    if ( ! $assoc['isCascadePersist']) {
975
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
976
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
977
                            . ' Explicitly persist the new document or configure cascading persist operations'
978
                            . ' on the relationship.');
979
                    }
980
981 68
                    $this->persistNew($targetClass, $entry);
982 68
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
983 68
                    $this->computeChangeSet($targetClass, $entry);
984 68
                    break;
985
986 365
                case self::STATE_MANAGED:
987 365
                    if ($targetClass->isEmbeddedDocument) {
988 178
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
989 178
                        if ($knownParent && $knownParent !== $parentDocument) {
990 9
                            $entry = clone $entry;
991 9
                            if ($assoc['type'] === ClassMetadata::ONE) {
992 6
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
993 6
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
994 6
                                $poid = spl_object_hash($parentDocument);
995 6
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
996 6
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
997
                                }
998
                            } else {
999
                                // must use unwrapped value to not trigger orphan removal
1000 7
                                $unwrappedValue[$key] = $entry;
1001
                            }
1002 9
                            $this->persistNew($targetClass, $entry);
1003
                        }
1004 178
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
1005 178
                        $this->computeChangeSet($targetClass, $entry);
1006
                    }
1007 365
                    break;
1008
1009 1
                case self::STATE_REMOVED:
1010
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1011
                    // and remove the element from Collection.
1012 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1013
                        unset($value[$key]);
1014
                    }
1015 1
                    break;
1016
1017
                case self::STATE_DETACHED:
1018
                    // Can actually not happen right now as we assume STATE_NEW,
1019
                    // so the exception will be raised from the DBAL layer (constraint violation).
1020
                    throw new \InvalidArgumentException('A detached document was found through a '
1021
                        . 'relationship during cascading a persist operation.');
1022
1023 370
                default:
1024
                    // MANAGED associated documents are already taken into account
1025
                    // during changeset calculation anyway, since they are in the identity map.
1026
1027
            }
1028
        }
1029 465
    }
1030
1031
    /**
1032
     * INTERNAL:
1033
     * Computes the changeset of an individual document, independently of the
1034
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1035
     *
1036
     * The passed document must be a managed document. If the document already has a change set
1037
     * because this method is invoked during a commit cycle then the change sets are added.
1038
     * whereby changes detected in this method prevail.
1039
     *
1040
     * @ignore
1041
     * @param ClassMetadata $class The class descriptor of the document.
1042
     * @param object $document The document for which to (re)calculate the change set.
1043
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1044
     */
1045 21
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1046
    {
1047
        // Ignore uninitialized proxy objects
1048 21
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1049 1
            return;
1050
        }
1051
1052 20
        $oid = spl_object_hash($document);
1053
1054 20
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1055
            throw new \InvalidArgumentException('Document must be managed.');
1056
        }
1057
1058 20
        if ( ! $class->isInheritanceTypeNone()) {
1059 2
            $class = $this->dm->getClassMetadata(get_class($document));
1060
        }
1061
1062 20
        $this->computeOrRecomputeChangeSet($class, $document, true);
1063 20
    }
1064
1065
    /**
1066
     * @param ClassMetadata $class
1067
     * @param object $document
1068
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1069
     */
1070 662
    private function persistNew(ClassMetadata $class, $document)
1071
    {
1072 662
        $this->lifecycleEventManager->prePersist($class, $document);
1073 662
        $oid = spl_object_hash($document);
1074 662
        $upsert = false;
1075 662
        if ($class->identifier) {
1076 662
            $idValue = $class->getIdentifierValue($document);
1077 662
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1078
1079 662
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1080 3
                throw new \InvalidArgumentException(sprintf(
1081 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1082 3
                    get_class($document)
1083
                ));
1084
            }
1085
1086 661
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1087 1
                throw new \InvalidArgumentException(sprintf(
1088 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1089 1
                    get_class($document)
1090
                ));
1091
            }
1092
1093 660
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1094 577
                $idValue = $class->idGenerator->generate($this->dm, $document);
1095 577
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1096 577
                $class->setIdentifierValue($document, $idValue);
1097
            }
1098
1099 660
            $this->documentIdentifiers[$oid] = $idValue;
1100
        } else {
1101
            // this is for embedded documents without identifiers
1102 161
            $this->documentIdentifiers[$oid] = $oid;
1103
        }
1104
1105 660
        $this->documentStates[$oid] = self::STATE_MANAGED;
1106
1107 660
        if ($upsert) {
1108 93
            $this->scheduleForUpsert($class, $document);
1109
        } else {
1110 586
            $this->scheduleForInsert($class, $document);
1111
        }
1112 660
    }
1113
1114
    /**
1115
     * Executes all document insertions for documents of the specified type.
1116
     *
1117
     * @param ClassMetadata $class
1118
     * @param array $documents Array of documents to insert
1119
     * @param array $options Array of options to be used with batchInsert()
1120
     */
1121 545 View Code Duplication
    private function executeInserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1122
    {
1123 545
        $persister = $this->getDocumentPersister($class->name);
1124
1125 545
        foreach ($documents as $oid => $document) {
1126 545
            $persister->addInsert($document);
1127 545
            unset($this->documentInsertions[$oid]);
1128
        }
1129
1130 545
        $persister->executeInserts($options);
1131
1132 544
        foreach ($documents as $document) {
1133 544
            $this->lifecycleEventManager->postPersist($class, $document);
1134
        }
1135 544
    }
1136
1137
    /**
1138
     * Executes all document upserts for documents of the specified type.
1139
     *
1140
     * @param ClassMetadata $class
1141
     * @param array $documents Array of documents to upsert
1142
     * @param array $options Array of options to be used with batchInsert()
1143
     */
1144 89 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1145
    {
1146 89
        $persister = $this->getDocumentPersister($class->name);
1147
1148
1149 89
        foreach ($documents as $oid => $document) {
1150 89
            $persister->addUpsert($document);
1151 89
            unset($this->documentUpserts[$oid]);
1152
        }
1153
1154 89
        $persister->executeUpserts($options);
1155
1156 89
        foreach ($documents as $document) {
1157 89
            $this->lifecycleEventManager->postPersist($class, $document);
1158
        }
1159 89
    }
1160
1161
    /**
1162
     * Executes all document updates for documents of the specified type.
1163
     *
1164
     * @param Mapping\ClassMetadata $class
1165
     * @param array $documents Array of documents to update
1166
     * @param array $options Array of options to be used with update()
1167
     */
1168 232
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1169
    {
1170 232
        if ($class->isReadOnly) {
1171
            return;
1172
        }
1173
1174 232
        $className = $class->name;
1175 232
        $persister = $this->getDocumentPersister($className);
1176
1177 232
        foreach ($documents as $oid => $document) {
1178 232
            $this->lifecycleEventManager->preUpdate($class, $document);
1179
1180 232
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1181 230
                $persister->update($document, $options);
1182
            }
1183
1184 228
            unset($this->documentUpdates[$oid]);
1185
1186 228
            $this->lifecycleEventManager->postUpdate($class, $document);
1187
        }
1188 227
    }
1189
1190
    /**
1191
     * Executes all document deletions for documents of the specified type.
1192
     *
1193
     * @param ClassMetadata $class
1194
     * @param array $documents Array of documents to delete
1195
     * @param array $options Array of options to be used with remove()
1196
     */
1197 72
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1198
    {
1199 72
        $persister = $this->getDocumentPersister($class->name);
1200
1201 72
        foreach ($documents as $oid => $document) {
1202 72
            if ( ! $class->isEmbeddedDocument) {
1203 34
                $persister->delete($document, $options);
1204
            }
1205
            unset(
1206 70
                $this->documentDeletions[$oid],
1207 70
                $this->documentIdentifiers[$oid],
1208 70
                $this->originalDocumentData[$oid]
1209
            );
1210
1211
            // Clear snapshot information for any referenced PersistentCollection
1212
            // http://www.doctrine-project.org/jira/browse/MODM-95
1213 70
            foreach ($class->associationMappings as $fieldMapping) {
1214 45
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1215 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1216 27
                    if ($value instanceof PersistentCollectionInterface) {
1217 45
                        $value->clearSnapshot();
1218
                    }
1219
                }
1220
            }
1221
1222
            // Document with this $oid after deletion treated as NEW, even if the $oid
1223
            // is obtained by a new document because the old one went out of scope.
1224 70
            $this->documentStates[$oid] = self::STATE_NEW;
1225
1226 70
            $this->lifecycleEventManager->postRemove($class, $document);
1227
        }
1228 70
    }
1229
1230
    /**
1231
     * Schedules a document for insertion into the database.
1232
     * If the document already has an identifier, it will be added to the
1233
     * identity map.
1234
     *
1235
     * @param ClassMetadata $class
1236
     * @param object $document The document to schedule for insertion.
1237
     * @throws \InvalidArgumentException
1238
     */
1239 589
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
1889
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1890
                                } else {
1891
                                    $other = $this
1892
                                        ->dm
1893
                                        ->getProxyFactory()
1894
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1895
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

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

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

Loading history...
2216
    {
2217 76
        $class = $this->dm->getClassMetadata(get_class($document));
2218 76
        foreach ($class->fieldMappings as $mapping) {
2219 76
            if ( ! $mapping['isCascadeRemove']) {
2220 75
                continue;
2221
            }
2222 36
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2223 2
                $document->__load();
2224
            }
2225
2226 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2227 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2228
                // If its a PersistentCollection initialization is intended! No unwrap!
2229 25
                foreach ($relatedDocuments as $relatedDocument) {
2230 25
                    $this->doRemove($relatedDocument, $visited);
2231
                }
2232 24
            } elseif ($relatedDocuments !== null) {
2233 36
                $this->doRemove($relatedDocuments, $visited);
2234
            }
2235
        }
2236 76
    }
2237
2238
    /**
2239
     * Acquire a lock on the given document.
2240
     *
2241
     * @param object $document
2242
     * @param int $lockMode
2243
     * @param int $lockVersion
2244
     * @throws LockException
2245
     * @throws \InvalidArgumentException
2246
     */
2247 9
    public function lock($document, $lockMode, $lockVersion = null)
2248
    {
2249 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2250 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2251
        }
2252
2253 8
        $documentName = get_class($document);
2254 8
        $class = $this->dm->getClassMetadata($documentName);
2255
2256 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2257 3
            if ( ! $class->isVersioned) {
2258 1
                throw LockException::notVersioned($documentName);
2259
            }
2260
2261 2
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2262 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2263 2
                if ($documentVersion != $lockVersion) {
2264 2
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2265
                }
2266
            }
2267 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2268 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2269
        }
2270 6
    }
2271
2272
    /**
2273
     * Releases a lock on the given document.
2274
     *
2275
     * @param object $document
2276
     * @throws \InvalidArgumentException
2277
     */
2278 1
    public function unlock($document)
2279
    {
2280 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2281
            throw new \InvalidArgumentException("Document is not MANAGED.");
2282
        }
2283 1
        $documentName = get_class($document);
2284 1
        $this->getDocumentPersister($documentName)->unlock($document);
2285 1
    }
2286
2287
    /**
2288
     * Clears the UnitOfWork.
2289
     *
2290
     * @param string|null $documentName if given, only documents of this type will get detached.
2291
     */
2292 422
    public function clear($documentName = null)
2293
    {
2294 422
        if ($documentName === null) {
2295 414
            $this->identityMap =
2296 414
            $this->documentIdentifiers =
2297 414
            $this->originalDocumentData =
2298 414
            $this->documentChangeSets =
2299 414
            $this->documentStates =
2300 414
            $this->scheduledForDirtyCheck =
2301 414
            $this->documentInsertions =
2302 414
            $this->documentUpserts =
2303 414
            $this->documentUpdates =
2304 414
            $this->documentDeletions =
2305 414
            $this->collectionUpdates =
2306 414
            $this->collectionDeletions =
2307 414
            $this->parentAssociations =
2308 414
            $this->embeddedDocumentsRegistry =
2309 414
            $this->orphanRemovals =
2310 414
            $this->hasScheduledCollections = array();
2311
        } else {
2312 8
            $visited = array();
2313 8
            foreach ($this->identityMap as $className => $documents) {
2314 8
                if ($className === $documentName) {
2315 5
                    foreach ($documents as $document) {
2316 8
                        $this->doDetach($document, $visited);
2317
                    }
2318
                }
2319
            }
2320
        }
2321
2322 422 View Code Duplication
        if ($this->evm->hasListeners(Events::onClear)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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