Completed
Pull Request — master (#1448)
by Andreas
11:14 queued 05:13
created

UnitOfWork::getOrCreateDocument()   D

Complexity

Conditions 15
Paths 261

Size

Total Lines 72
Code Lines 47

Duplication

Lines 5
Ratio 6.94 %

Code Coverage

Tests 44
CRAP Score 15.0024

Importance

Changes 2
Bugs 0 Features 2
Metric Value
c 2
b 0
f 2
dl 5
loc 72
ccs 44
cts 45
cp 0.9778
rs 4.1144
cc 15
eloc 47
nc 261
nop 4
crap 15.0024

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\EventManager;
25
use Doctrine\Common\NotifyPropertyChanged;
26
use Doctrine\Common\PropertyChangedListener;
27
use Doctrine\MongoDB\GridFSFile;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
30
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
31
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\Query;
34
use Doctrine\ODM\MongoDB\Types\Type;
35
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
36
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
37
38
/**
39
 * The UnitOfWork is responsible for tracking changes to objects during an
40
 * "object-level" transaction and for writing out changes to the database
41
 * in the correct order.
42
 *
43
 * @since       1.0
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = array();
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = array();
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @var array
101
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
102
     *           A value will only really be copied if the value in the document is modified
103
     *           by the user.
104
     */
105
    private $originalDocumentData = array();
106
107
    /**
108
     * Map of document changes. Keys are object ids (spl_object_hash).
109
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
110
     *
111
     * @var array
112
     */
113
    private $documentChangeSets = array();
114
115
    /**
116
     * The (cached) states of any known documents.
117
     * Keys are object ids (spl_object_hash).
118
     *
119
     * @var array
120
     */
121
    private $documentStates = array();
122
123
    /**
124
     * Map of documents that are scheduled for dirty checking at commit time.
125
     *
126
     * Documents are grouped by their class name, and then indexed by their SPL
127
     * object hash. This is only used for documents with a change tracking
128
     * policy of DEFERRED_EXPLICIT.
129
     *
130
     * @var array
131
     * @todo rename: scheduledForSynchronization
132
     */
133
    private $scheduledForDirtyCheck = array();
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = array();
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = array();
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = array();
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = array();
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = array();
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = array();
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = array();
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = array();
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = array();
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = array();
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents.
244
     *
245
     * @var array
246
     */
247
    private $parentAssociations = array();
248
249
    /**
250
     * @var LifecycleEventManager
251
     */
252
    private $lifecycleEventManager;
253
254
    /**
255
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
256
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
257
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
258
     *
259
     * @var array
260
     */
261
    private $embeddedDocumentsRegistry = array();
262
263
    /**
264
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
265
     *
266
     * @param DocumentManager $dm
267
     * @param EventManager $evm
268
     * @param HydratorFactory $hydratorFactory
269
     */
270 1044
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
271
    {
272 1044
        $this->dm = $dm;
273 1044
        $this->evm = $evm;
274 1044
        $this->hydratorFactory = $hydratorFactory;
275 1044
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
276 1044
    }
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 729
    public function getPersistenceBuilder()
285
    {
286 729
        if ( ! $this->persistenceBuilder) {
287 729
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
288
        }
289 729
        return $this->persistenceBuilder;
290
    }
291
292
    /**
293
     * Sets the parent association for a given embedded document.
294
     *
295
     * @param object $document
296
     * @param array $mapping
297
     * @param object $parent
298
     * @param string $propertyPath
299
     */
300 199
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
301
    {
302 199
        $oid = spl_object_hash($document);
303 199
        $this->embeddedDocumentsRegistry[$oid] = $document;
304 199
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
305 199
    }
306
307
    /**
308
     * Gets the parent association for a given embedded document.
309
     *
310
     *     <code>
311
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
312
     *     </code>
313
     *
314
     * @param object $document
315
     * @return array $association
316
     */
317 229
    public function getParentAssociation($document)
318
    {
319 229
        $oid = spl_object_hash($document);
320 229
        if ( ! isset($this->parentAssociations[$oid])) {
321 224
            return null;
322
        }
323 178
        return $this->parentAssociations[$oid];
324
    }
325
326
    /**
327
     * Get the document persister instance for the given document name
328
     *
329
     * @param string $documentName
330
     * @return Persisters\DocumentPersister
331
     */
332 727
    public function getDocumentPersister($documentName)
333
    {
334 727
        if ( ! isset($this->persisters[$documentName])) {
335 713
            $class = $this->dm->getClassMetadata($documentName);
336 713
            $pb = $this->getPersistenceBuilder();
337 713
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
338
        }
339 727
        return $this->persisters[$documentName];
340
    }
341
342
    /**
343
     * Get the collection persister instance.
344
     *
345
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
346
     */
347 727
    public function getCollectionPersister()
348
    {
349 727
        if ( ! isset($this->collectionPersister)) {
350 727
            $pb = $this->getPersistenceBuilder();
351 727
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
352
        }
353 727
        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 604
    public function commit($document = null, array $options = array())
382
    {
383
        // Raise preFlush
384 604
        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 604
        if ($document === null) {
390 598
            $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 602
        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 258
            $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 215
            $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 201
            $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 24
            $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 24
            $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 602
            $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 24
            return; // Nothing to do.
408
        }
409
410 599
        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 48
            foreach ($this->orphanRemovals as $removal) {
412 48
                $this->remove($removal);
413
            }
414
        }
415
416
        // Raise onFlush
417 599
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419
        }
420
421 599
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 86
            list($class, $documents) = $classAndDocuments;
423 86
            $this->executeUpserts($class, $documents, $options);
424
        }
425
426 599
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 524
            list($class, $documents) = $classAndDocuments;
428 524
            $this->executeInserts($class, $documents, $options);
429
        }
430
431 598
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 230
            list($class, $documents) = $classAndDocuments;
433 230
            $this->executeUpdates($class, $documents, $options);
434
        }
435
436 598
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 69
            list($class, $documents) = $classAndDocuments;
438 69
            $this->executeDeletions($class, $documents, $options);
439
        }
440
441
        // Raise postFlush
442 598
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 598
        $this->documentInsertions =
448 598
        $this->documentUpserts =
449 598
        $this->documentUpdates =
450 598
        $this->documentDeletions =
451 598
        $this->documentChangeSets =
452 598
        $this->collectionUpdates =
453 598
        $this->collectionDeletions =
454 598
        $this->visitedCollections =
455 598
        $this->scheduledForDirtyCheck =
456 598
        $this->orphanRemovals =
457 598
        $this->hasScheduledCollections = array();
458 598
    }
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 599
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 599
        if (empty($documents)) {
470 599
            return array();
471
        }
472 598
        $divided = array();
473 598
        $embeds = array();
474 598
        foreach ($documents as $oid => $d) {
475 598
            $className = get_class($d);
476 598
            if (isset($embeds[$className])) {
477 77
                continue;
478
            }
479 598
            if (isset($divided[$className])) {
480 147
                $divided[$className][1][$oid] = $d;
481 147
                continue;
482
            }
483 598
            $class = $this->dm->getClassMetadata($className);
484 598
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 180
                $embeds[$className] = true;
486 180
                continue;
487
            }
488 598
            if (empty($divided[$class->name])) {
489 598
                $divided[$class->name] = array($class, array($oid => $d));
490
            } else {
491 598
                $divided[$class->name][1][$oid] = $d;
492
            }
493
        }
494 598
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 606 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 606
        foreach ($this->documentInsertions as $document) {
505 534
            $class = $this->dm->getClassMetadata(get_class($document));
506 534
            if ( ! $class->isEmbeddedDocument) {
507 534
                $this->computeChangeSet($class, $document);
508
            }
509
        }
510 605
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 605 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 605
        foreach ($this->documentUpserts as $document) {
520 85
            $class = $this->dm->getClassMetadata(get_class($document));
521 85
            if ( ! $class->isEmbeddedDocument) {
522 85
                $this->computeChangeSet($class, $document);
523
            }
524
        }
525 605
    }
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 599
    public function getDocumentChangeSet($document)
580
    {
581 599
        $oid = spl_object_hash($document);
582 599
        if (isset($this->documentChangeSets[$oid])) {
583 596
            return $this->documentChangeSets[$oid];
584
        }
585 60
        return array();
586
    }
587
588
    /**
589
     * INTERNAL:
590
     * Sets the changeset for a document.
591
     *
592
     * @param object $document
593
     * @param array $changeset
594
     */
595 1
    public function setDocumentChangeSet($document, $changeset)
596
    {
597 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
598 1
    }
599
600
    /**
601
     * Get a documents actual data, flattening all the objects to arrays.
602
     *
603
     * @param object $document
604
     * @return array
605
     */
606 606
    public function getDocumentActualData($document)
607
    {
608 606
        $class = $this->dm->getClassMetadata(get_class($document));
609 606
        $actualData = array();
610 606
        foreach ($class->reflFields as $name => $refProp) {
611 606
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 606
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 51
                continue;
615
            }
616 606
            $value = $refProp->getValue($document);
617 606
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
618 6
                $value = new GridFSFile($value);
619 6
                $class->reflFields[$name]->setValue($document, $value);
620 6
                $actualData[$name] = $value;
621 606
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 606
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 392
                if ( ! $value instanceof Collection) {
625 131
                    $value = new ArrayCollection($value);
626
                }
627
628
                // Inject PersistentCollection
629 392
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
630 392
                $coll->setOwner($document, $mapping);
631 392
                $coll->setDirty( ! $value->isEmpty());
632 392
                $class->reflFields[$name]->setValue($document, $coll);
633 392
                $actualData[$name] = $coll;
634
            } else {
635 606
                $actualData[$name] = $value;
636
            }
637
        }
638 606
        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 603
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 603
        if ( ! $class->isInheritanceTypeNone()) {
668 180
            $class = $this->dm->getClassMetadata(get_class($document));
669
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 603 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 603
        $this->computeOrRecomputeChangeSet($class, $document);
677 602
    }
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 603
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 603
        $oid = spl_object_hash($document);
689 603
        $actualData = $this->getDocumentActualData($document);
690 603
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 603
        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 603
            $this->originalDocumentData[$oid] = $actualData;
695 603
            $changeSet = array();
696 603
            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 603
                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 603 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 185
                    continue;
707
                }
708 603
                $changeSet[$propName] = array(null, $actualValue);
709
            }
710 603
            $this->documentChangeSets[$oid] = $changeSet;
711
        } else {
712
            // Document is "fully" MANAGED: it was already fully persisted before
713
            // and we have a copy of the original data
714 290
            $originalData = $this->originalDocumentData[$oid];
715 290
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
716 290
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
717 2
                $changeSet = $this->documentChangeSets[$oid];
718
            } else {
719 290
                $changeSet = array();
720
            }
721
722 290
            foreach ($actualData as $propName => $actualValue) {
723
                // skip not saved fields
724 290
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
725
                    continue;
726
                }
727
728 290
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
729
730
                // skip if value has not changed
731 290
                if ($orgValue === $actualValue) {
732 289
                    if ($actualValue instanceof PersistentCollectionInterface) {
733 203
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
734
                            // consider dirty collections as changed as well
735 203
                            continue;
736
                        }
737 289
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
738
                        // but consider dirty GridFSFile instances as changed
739 289
                        continue;
740
                    }
741
                }
742
743
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
744 249
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
745 12
                    if ($orgValue !== null) {
746 7
                        $this->scheduleOrphanRemoval($orgValue);
747
                    }
748
749 12
                    $changeSet[$propName] = array($orgValue, $actualValue);
750 12
                    continue;
751
                }
752
753
                // if owning side of reference-one relationship
754 242
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
755 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
756 1
                        $this->scheduleOrphanRemoval($orgValue);
757
                    }
758
759 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
760 13
                    continue;
761
                }
762
763 235
                if ($isChangeTrackingNotify) {
764 3
                    continue;
765
                }
766
767
                // ignore inverse side of reference relationship
768 233 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
769 6
                    continue;
770
                }
771
772
                // Persistent collection was exchanged with the "originally"
773
                // created one. This can only mean it was cloned and replaced
774
                // on another document.
775 231
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
776 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
777
                }
778
779
                // if embed-many or reference-many relationship
780 231
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
781 117
                    $changeSet[$propName] = array($orgValue, $actualValue);
782
                    /* If original collection was exchanged with a non-empty value
783
                     * and $set will be issued, there is no need to $unset it first
784
                     */
785 117
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
786 28
                        continue;
787
                    }
788 97
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
789 17
                        $this->scheduleCollectionDeletion($orgValue);
790
                    }
791 97
                    continue;
792
                }
793
794
                // skip equivalent date values
795 151
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
796 36
                    $dateType = Type::getType('date');
797 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
798 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
799
800 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
801 29
                        continue;
802
                    }
803
                }
804
805
                // regular field
806 135
                $changeSet[$propName] = array($orgValue, $actualValue);
807
            }
808 290
            if ($changeSet) {
809 238
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
810 21
                    ? $changeSet + $this->documentChangeSets[$oid]
811 233
                    : $changeSet;
812
813 238
                $this->originalDocumentData[$oid] = $actualData;
814 238
                $this->scheduleForUpdate($document);
815
            }
816
        }
817
818
        // Look for changes in associations of the document
819 603
        $associationMappings = array_filter(
820 603
            $class->associationMappings,
821
            function ($assoc) { return empty($assoc['notSaved']); }
822
        );
823
824 603
        foreach ($associationMappings as $mapping) {
825 462
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
826
827 462
            if ($value === null) {
828 310
                continue;
829
            }
830
831 449
            $this->computeAssociationChanges($document, $mapping, $value);
832
833 448
            if (isset($mapping['reference'])) {
834 338
                continue;
835
            }
836
837 349
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
838
839 349
            foreach ($values as $obj) {
840 184
                $oid2 = spl_object_hash($obj);
841
842 184
                if (isset($this->documentChangeSets[$oid2])) {
843 182
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
844
845 182
                    if ( ! $isNewDocument) {
846 79
                        $this->scheduleForUpdate($document);
847
                    }
848
849 349
                    break;
850
                }
851
            }
852
        }
853 602
    }
854
855
    /**
856
     * Computes all the changes that have been done to documents and collections
857
     * since the last commit and stores these changes in the _documentChangeSet map
858
     * temporarily for access by the persisters, until the UoW commit is finished.
859
     */
860 601
    public function computeChangeSets()
861
    {
862 601
        $this->computeScheduleInsertsChangeSets();
863 600
        $this->computeScheduleUpsertsChangeSets();
864
865
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
866 600
        foreach ($this->identityMap as $className => $documents) {
867 600
            $class = $this->dm->getClassMetadata($className);
868 600
            if ($class->isEmbeddedDocument) {
869
                /* we do not want to compute changes to embedded documents up front
870
                 * in case embedded document was replaced and its changeset
871
                 * would corrupt data. Embedded documents' change set will
872
                 * be calculated by reachability from owning document.
873
                 */
874 172
                continue;
875
            }
876
877
            // If change tracking is explicit or happens through notification, then only compute
878
            // changes on document of that type that are explicitly marked for synchronization.
879
            switch (true) {
880 600
                case ($class->isChangeTrackingDeferredImplicit()):
881 599
                    $documentsToProcess = $documents;
882 599
                    break;
883
884 4
                case (isset($this->scheduledForDirtyCheck[$className])):
885 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
886 3
                    break;
887
888
                default:
889 4
                    $documentsToProcess = array();
890
891
            }
892
893 600
            foreach ($documentsToProcess as $document) {
894
                // Ignore uninitialized proxy objects
895 596
                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...
896 10
                    continue;
897
                }
898
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
899 596
                $oid = spl_object_hash($document);
900 596 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...
901 596
                    && ! isset($this->documentUpserts[$oid])
902 596
                    && ! isset($this->documentDeletions[$oid])
903 596
                    && isset($this->documentStates[$oid])
904
                ) {
905 600
                    $this->computeChangeSet($class, $document);
906
                }
907
            }
908
        }
909 600
    }
910
911
    /**
912
     * Computes the changes of an association.
913
     *
914
     * @param object $parentDocument
915
     * @param array $assoc
916
     * @param mixed $value The value of the association.
917
     * @throws \InvalidArgumentException
918
     */
919 449
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
920
    {
921 449
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
922 449
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
923 449
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
924
925 449
        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...
926 8
            return;
927
        }
928
929 448
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
930 247
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
931 243
                $this->scheduleCollectionUpdate($value);
932
            }
933 247
            $topmostOwner = $this->getOwningDocument($value->getOwner());
934 247
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
935 247
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
936 145
                $value->initialize();
937 145
                foreach ($value->getDeletedDocuments() as $orphan) {
938 23
                    $this->scheduleOrphanRemoval($orphan);
939
                }
940
            }
941
        }
942
943
        // Look through the documents, and in any of their associations,
944
        // for transient (new) documents, recursively. ("Persistence by reachability")
945
        // Unwrap. Uninitialized collections will simply be empty.
946 448
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
947
948 448
        $count = 0;
949 448
        foreach ($unwrappedValue as $key => $entry) {
950 353
            if ( ! is_object($entry)) {
951 1
                throw new \InvalidArgumentException(
952 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
953
                );
954
            }
955
956 352
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
957
958 352
            $state = $this->getDocumentState($entry, self::STATE_NEW);
959
960
            // Handle "set" strategy for multi-level hierarchy
961 352
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
962 352
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
963
964 352
            $count++;
965
966
            switch ($state) {
967 352
                case self::STATE_NEW:
968 63
                    if ( ! $assoc['isCascadePersist']) {
969
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
970
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
971
                            . ' Explicitly persist the new document or configure cascading persist operations'
972
                            . ' on the relationship.');
973
                    }
974
975 63
                    $this->persistNew($targetClass, $entry);
976 63
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
977 63
                    $this->computeChangeSet($targetClass, $entry);
978 63
                    break;
979
980 347
                case self::STATE_MANAGED:
981 347
                    if ($targetClass->isEmbeddedDocument) {
982 175
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
983 175
                        if ($knownParent && $knownParent !== $parentDocument) {
984 7
                            $entry = clone $entry;
985 7
                            if ($assoc['type'] === ClassMetadata::ONE) {
986 4
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
987 4
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
988
                            } else {
989
                                // must use unwrapped value to not trigger orphan removal
990 6
                                $unwrappedValue[$key] = $entry;
991
                            }
992 7
                            $this->persistNew($targetClass, $entry);
993
                        }
994 175
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
995 175
                        $this->computeChangeSet($targetClass, $entry);
996
                    }
997 347
                    break;
998
999 1
                case self::STATE_REMOVED:
1000
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1001
                    // and remove the element from Collection.
1002 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1003
                        unset($value[$key]);
1004
                    }
1005 1
                    break;
1006
1007
                case self::STATE_DETACHED:
1008
                    // Can actually not happen right now as we assume STATE_NEW,
1009
                    // so the exception will be raised from the DBAL layer (constraint violation).
1010
                    throw new \InvalidArgumentException('A detached document was found through a '
1011
                        . 'relationship during cascading a persist operation.');
1012
1013 352
                default:
1014
                    // MANAGED associated documents are already taken into account
1015
                    // during changeset calculation anyway, since they are in the identity map.
1016
1017
            }
1018
        }
1019 447
    }
1020
1021
    /**
1022
     * INTERNAL:
1023
     * Computes the changeset of an individual document, independently of the
1024
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1025
     *
1026
     * The passed document must be a managed document. If the document already has a change set
1027
     * because this method is invoked during a commit cycle then the change sets are added.
1028
     * whereby changes detected in this method prevail.
1029
     *
1030
     * @ignore
1031
     * @param ClassMetadata $class The class descriptor of the document.
1032
     * @param object $document The document for which to (re)calculate the change set.
1033
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1034
     */
1035 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1036
    {
1037
        // Ignore uninitialized proxy objects
1038 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
1112
    {
1113 524
        $persister = $this->getDocumentPersister($class->name);
1114
1115 524
        foreach ($documents as $oid => $document) {
1116 524
            $persister->addInsert($document);
1117 524
            unset($this->documentInsertions[$oid]);
1118
        }
1119
1120 524
        $persister->executeInserts($options);
1121
1122 523
        foreach ($documents as $document) {
1123 523
            $this->lifecycleEventManager->postPersist($class, $document);
1124
        }
1125 523
    }
1126
1127
    /**
1128
     * Executes all document upserts for documents of the specified type.
1129
     *
1130
     * @param ClassMetadata $class
1131
     * @param array $documents Array of documents to upsert
1132
     * @param array $options Array of options to be used with batchInsert()
1133
     */
1134 86 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1135
    {
1136 86
        $persister = $this->getDocumentPersister($class->name);
1137
1138
1139 86
        foreach ($documents as $oid => $document) {
1140 86
            $persister->addUpsert($document);
1141 86
            unset($this->documentUpserts[$oid]);
1142
        }
1143
1144 86
        $persister->executeUpserts($options);
1145
1146 86
        foreach ($documents as $document) {
1147 86
            $this->lifecycleEventManager->postPersist($class, $document);
1148
        }
1149 86
    }
1150
1151
    /**
1152
     * Executes all document updates for documents of the specified type.
1153
     *
1154
     * @param Mapping\ClassMetadata $class
1155
     * @param array $documents Array of documents to update
1156
     * @param array $options Array of options to be used with update()
1157
     */
1158 230
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1159
    {
1160 230
        $className = $class->name;
1161 230
        $persister = $this->getDocumentPersister($className);
1162
1163 230
        foreach ($documents as $oid => $document) {
1164 230
            $this->lifecycleEventManager->preUpdate($class, $document);
1165
1166 230
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1167 228
                $persister->update($document, $options);
1168
            }
1169
1170 226
            unset($this->documentUpdates[$oid]);
1171
1172 226
            $this->lifecycleEventManager->postUpdate($class, $document);
1173
        }
1174 225
    }
1175
1176
    /**
1177
     * Executes all document deletions for documents of the specified type.
1178
     *
1179
     * @param ClassMetadata $class
1180
     * @param array $documents Array of documents to delete
1181
     * @param array $options Array of options to be used with remove()
1182
     */
1183 69
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1184
    {
1185 69
        $persister = $this->getDocumentPersister($class->name);
1186
1187 69
        foreach ($documents as $oid => $document) {
1188 69
            if ( ! $class->isEmbeddedDocument) {
1189 32
                $persister->delete($document, $options);
1190
            }
1191
            unset(
1192 67
                $this->documentDeletions[$oid],
1193 67
                $this->documentIdentifiers[$oid],
1194 67
                $this->originalDocumentData[$oid]
1195
            );
1196
1197
            // Clear snapshot information for any referenced PersistentCollection
1198
            // http://www.doctrine-project.org/jira/browse/MODM-95
1199 67
            foreach ($class->associationMappings as $fieldMapping) {
1200 44
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1201 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1202 27
                    if ($value instanceof PersistentCollectionInterface) {
1203 44
                        $value->clearSnapshot();
1204
                    }
1205
                }
1206
            }
1207
1208
            // Document with this $oid after deletion treated as NEW, even if the $oid
1209
            // is obtained by a new document because the old one went out of scope.
1210 67
            $this->documentStates[$oid] = self::STATE_NEW;
1211
1212 67
            $this->lifecycleEventManager->postRemove($class, $document);
1213
        }
1214 67
    }
1215
1216
    /**
1217
     * Schedules a document for insertion into the database.
1218
     * If the document already has an identifier, it will be added to the
1219
     * identity map.
1220
     *
1221
     * @param ClassMetadata $class
1222
     * @param object $document The document to schedule for insertion.
1223
     * @throws \InvalidArgumentException
1224
     */
1225 560
    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...
1226
    {
1227 560
        $oid = spl_object_hash($document);
1228
1229 560
        if (isset($this->documentUpdates[$oid])) {
1230
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1231
        }
1232 560
        if (isset($this->documentDeletions[$oid])) {
1233
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1234
        }
1235 560
        if (isset($this->documentInsertions[$oid])) {
1236
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1237
        }
1238
1239 560
        $this->documentInsertions[$oid] = $document;
1240
1241 560
        if (isset($this->documentIdentifiers[$oid])) {
1242 557
            $this->addToIdentityMap($document);
1243
        }
1244 560
    }
1245
1246
    /**
1247
     * Schedules a document for upsert into the database and adds it to the
1248
     * identity map
1249
     *
1250
     * @param ClassMetadata $class
1251
     * @param object $document The document to schedule for upsert.
1252
     * @throws \InvalidArgumentException
1253
     */
1254 92
    public function scheduleForUpsert(ClassMetadata $class, $document)
1255
    {
1256 92
        $oid = spl_object_hash($document);
1257
1258 92
        if ($class->isEmbeddedDocument) {
1259
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1260
        }
1261 92
        if (isset($this->documentUpdates[$oid])) {
1262
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1263
        }
1264 92
        if (isset($this->documentDeletions[$oid])) {
1265
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1266
        }
1267 92
        if (isset($this->documentUpserts[$oid])) {
1268
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1269
        }
1270
1271 92
        $this->documentUpserts[$oid] = $document;
1272 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1273 92
        $this->addToIdentityMap($document);
1274 92
    }
1275
1276
    /**
1277
     * Checks whether a document is scheduled for insertion.
1278
     *
1279
     * @param object $document
1280
     * @return boolean
1281
     */
1282 105
    public function isScheduledForInsert($document)
1283
    {
1284 105
        return isset($this->documentInsertions[spl_object_hash($document)]);
1285
    }
1286
1287
    /**
1288
     * Checks whether a document is scheduled for upsert.
1289
     *
1290
     * @param object $document
1291
     * @return boolean
1292
     */
1293 5
    public function isScheduledForUpsert($document)
1294
    {
1295 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1296
    }
1297
1298
    /**
1299
     * Schedules a document for being updated.
1300
     *
1301
     * @param object $document The document to schedule for being updated.
1302
     * @throws \InvalidArgumentException
1303
     */
1304 239
    public function scheduleForUpdate($document)
1305
    {
1306 239
        $oid = spl_object_hash($document);
1307 239
        if ( ! isset($this->documentIdentifiers[$oid])) {
1308
            throw new \InvalidArgumentException('Document has no identity.');
1309
        }
1310
1311 239
        if (isset($this->documentDeletions[$oid])) {
1312
            throw new \InvalidArgumentException('Document is removed.');
1313
        }
1314
1315 239
        if ( ! isset($this->documentUpdates[$oid])
1316 239
            && ! isset($this->documentInsertions[$oid])
1317 239
            && ! isset($this->documentUpserts[$oid])) {
1318 235
            $this->documentUpdates[$oid] = $document;
1319
        }
1320 239
    }
1321
1322
    /**
1323
     * Checks whether a document is registered as dirty in the unit of work.
1324
     * Note: Is not very useful currently as dirty documents are only registered
1325
     * at commit time.
1326
     *
1327
     * @param object $document
1328
     * @return boolean
1329
     */
1330 16
    public function isScheduledForUpdate($document)
1331
    {
1332 16
        return isset($this->documentUpdates[spl_object_hash($document)]);
1333
    }
1334
1335 1
    public function isScheduledForDirtyCheck($document)
1336
    {
1337 1
        $class = $this->dm->getClassMetadata(get_class($document));
1338 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1339
    }
1340
1341
    /**
1342
     * INTERNAL:
1343
     * Schedules a document for deletion.
1344
     *
1345
     * @param object $document
1346
     */
1347 74
    public function scheduleForDelete($document)
1348
    {
1349 74
        $oid = spl_object_hash($document);
1350
1351 74
        if (isset($this->documentInsertions[$oid])) {
1352 2
            if ($this->isInIdentityMap($document)) {
1353 2
                $this->removeFromIdentityMap($document);
1354
            }
1355 2
            unset($this->documentInsertions[$oid]);
1356 2
            return; // document has not been persisted yet, so nothing more to do.
1357
        }
1358
1359 73
        if ( ! $this->isInIdentityMap($document)) {
1360 2
            return; // ignore
1361
        }
1362
1363 72
        $this->removeFromIdentityMap($document);
1364 72
        $this->documentStates[$oid] = self::STATE_REMOVED;
1365
1366 72
        if (isset($this->documentUpdates[$oid])) {
1367
            unset($this->documentUpdates[$oid]);
1368
        }
1369 72
        if ( ! isset($this->documentDeletions[$oid])) {
1370 72
            $this->documentDeletions[$oid] = $document;
1371
        }
1372 72
    }
1373
1374
    /**
1375
     * Checks whether a document is registered as removed/deleted with the unit
1376
     * of work.
1377
     *
1378
     * @param object $document
1379
     * @return boolean
1380
     */
1381 8
    public function isScheduledForDelete($document)
1382
    {
1383 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1384
    }
1385
1386
    /**
1387
     * Checks whether a document is scheduled for insertion, update or deletion.
1388
     *
1389
     * @param $document
1390
     * @return boolean
1391
     */
1392 246
    public function isDocumentScheduled($document)
1393
    {
1394 246
        $oid = spl_object_hash($document);
1395 246
        return isset($this->documentInsertions[$oid]) ||
1396 130
            isset($this->documentUpserts[$oid]) ||
1397 120
            isset($this->documentUpdates[$oid]) ||
1398 246
            isset($this->documentDeletions[$oid]);
1399
    }
1400
1401
    /**
1402
     * INTERNAL:
1403
     * Registers a document in the identity map.
1404
     *
1405
     * Note that documents in a hierarchy are registered with the class name of
1406
     * the root document. Identifiers are serialized before being used as array
1407
     * keys to allow differentiation of equal, but not identical, values.
1408
     *
1409
     * @ignore
1410
     * @param object $document  The document to register.
1411
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1412
     *                  the document in question is already managed.
1413
     */
1414 660
    public function addToIdentityMap($document)
1415
    {
1416 660
        $class = $this->dm->getClassMetadata(get_class($document));
1417 660
        $id = $this->getIdForIdentityMap($document);
1418
1419 660
        if (isset($this->identityMap[$class->name][$id])) {
1420 56
            return false;
1421
        }
1422
1423 660
        $this->identityMap[$class->name][$id] = $document;
1424
1425 660
        if ($document instanceof NotifyPropertyChanged &&
1426 660
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1427 4
            $document->addPropertyChangedListener($this);
1428
        }
1429
1430 660
        return true;
1431
    }
1432
1433
    /**
1434
     * Gets the state of a document with regard to the current unit of work.
1435
     *
1436
     * @param object   $document
1437
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1438
     *                         This parameter can be set to improve performance of document state detection
1439
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1440
     *                         is either known or does not matter for the caller of the method.
1441
     * @return int The document state.
1442
     */
1443 634
    public function getDocumentState($document, $assume = null)
1444
    {
1445 634
        $oid = spl_object_hash($document);
1446
1447 634
        if (isset($this->documentStates[$oid])) {
1448 394
            return $this->documentStates[$oid];
1449
        }
1450
1451 634
        $class = $this->dm->getClassMetadata(get_class($document));
1452
1453 634
        if ($class->isEmbeddedDocument) {
1454 193
            return self::STATE_NEW;
1455
        }
1456
1457 631
        if ($assume !== null) {
1458 628
            return $assume;
1459
        }
1460
1461
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1462
         * known. Note that you cannot remember the NEW or DETACHED state in
1463
         * _documentStates since the UoW does not hold references to such
1464
         * objects and the object hash can be reused. More generally, because
1465
         * the state may "change" between NEW/DETACHED without the UoW being
1466
         * aware of it.
1467
         */
1468 4
        $id = $class->getIdentifierObject($document);
1469
1470 4
        if ($id === null) {
1471 3
            return self::STATE_NEW;
1472
        }
1473
1474
        // Check for a version field, if available, to avoid a DB lookup.
1475 2
        if ($class->isVersioned) {
1476
            return $class->getFieldValue($document, $class->versionField)
1477
                ? self::STATE_DETACHED
1478
                : self::STATE_NEW;
1479
        }
1480
1481
        // Last try before DB lookup: check the identity map.
1482 2
        if ($this->tryGetById($id, $class)) {
1483 1
            return self::STATE_DETACHED;
1484
        }
1485
1486
        // DB lookup
1487 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1488 1
            return self::STATE_DETACHED;
1489
        }
1490
1491 1
        return self::STATE_NEW;
1492
    }
1493
1494
    /**
1495
     * INTERNAL:
1496
     * Removes a document from the identity map. This effectively detaches the
1497
     * document from the persistence management of Doctrine.
1498
     *
1499
     * @ignore
1500
     * @param object $document
1501
     * @throws \InvalidArgumentException
1502
     * @return boolean
1503
     */
1504 87
    public function removeFromIdentityMap($document)
1505
    {
1506 87
        $oid = spl_object_hash($document);
1507
1508
        // Check if id is registered first
1509 87
        if ( ! isset($this->documentIdentifiers[$oid])) {
1510
            return false;
1511
        }
1512
1513 87
        $class = $this->dm->getClassMetadata(get_class($document));
1514 87
        $id = $this->getIdForIdentityMap($document);
1515
1516 87
        if (isset($this->identityMap[$class->name][$id])) {
1517 87
            unset($this->identityMap[$class->name][$id]);
1518 87
            $this->documentStates[$oid] = self::STATE_DETACHED;
1519 87
            return true;
1520
        }
1521
1522
        return false;
1523
    }
1524
1525
    /**
1526
     * INTERNAL:
1527
     * Gets a document in the identity map by its identifier hash.
1528
     *
1529
     * @ignore
1530
     * @param mixed         $id    Document identifier
1531
     * @param ClassMetadata $class Document class
1532
     * @return object
1533
     * @throws InvalidArgumentException if the class does not have an identifier
1534
     */
1535 34
    public function getById($id, ClassMetadata $class)
1536
    {
1537 34
        if ( ! $class->identifier) {
1538
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1539
        }
1540
1541 34
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1542
1543 34
        return $this->identityMap[$class->name][$serializedId];
1544
    }
1545
1546
    /**
1547
     * INTERNAL:
1548
     * Tries to get a document by its identifier hash. If no document is found
1549
     * for the given hash, FALSE is returned.
1550
     *
1551
     * @ignore
1552
     * @param mixed         $id    Document identifier
1553
     * @param ClassMetadata $class Document class
1554
     * @return mixed The found document or FALSE.
1555
     * @throws InvalidArgumentException if the class does not have an identifier
1556
     */
1557 307
    public function tryGetById($id, ClassMetadata $class)
1558
    {
1559 307
        if ( ! $class->identifier) {
1560
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1561
        }
1562
1563 307
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1564
1565 307
        return isset($this->identityMap[$class->name][$serializedId]) ?
1566 307
            $this->identityMap[$class->name][$serializedId] : false;
1567
    }
1568
1569
    /**
1570
     * Schedules a document for dirty-checking at commit-time.
1571
     *
1572
     * @param object $document The document to schedule for dirty-checking.
1573
     * @todo Rename: scheduleForSynchronization
1574
     */
1575 3
    public function scheduleForDirtyCheck($document)
1576
    {
1577 3
        $class = $this->dm->getClassMetadata(get_class($document));
1578 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1579 3
    }
1580
1581
    /**
1582
     * Checks whether a document is registered in the identity map.
1583
     *
1584
     * @param object $document
1585
     * @return boolean
1586
     */
1587 85
    public function isInIdentityMap($document)
1588
    {
1589 85
        $oid = spl_object_hash($document);
1590
1591 85
        if ( ! isset($this->documentIdentifiers[$oid])) {
1592 6
            return false;
1593
        }
1594
1595 83
        $class = $this->dm->getClassMetadata(get_class($document));
1596 83
        $id = $this->getIdForIdentityMap($document);
1597
1598 83
        return isset($this->identityMap[$class->name][$id]);
1599
    }
1600
1601
    /**
1602
     * @param object $document
1603
     * @return string
1604
     */
1605 660
    private function getIdForIdentityMap($document)
1606
    {
1607 660
        $class = $this->dm->getClassMetadata(get_class($document));
1608
1609 660
        if ( ! $class->identifier) {
1610 161
            $id = spl_object_hash($document);
1611
        } else {
1612 659
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1613 659
            $id = serialize($class->getDatabaseIdentifierValue($id));
1614
        }
1615
1616 660
        return $id;
1617
    }
1618
1619
    /**
1620
     * INTERNAL:
1621
     * Checks whether an identifier exists in the identity map.
1622
     *
1623
     * @ignore
1624
     * @param string $id
1625
     * @param string $rootClassName
1626
     * @return boolean
1627
     */
1628
    public function containsId($id, $rootClassName)
1629
    {
1630
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1631
    }
1632
1633
    /**
1634
     * Persists a document as part of the current unit of work.
1635
     *
1636
     * @param object $document The document to persist.
1637
     * @throws MongoDBException If trying to persist MappedSuperclass.
1638
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1639
     */
1640 628
    public function persist($document)
1641
    {
1642 628
        $class = $this->dm->getClassMetadata(get_class($document));
1643 628
        if ($class->isMappedSuperclass || $class->isAggregationResultDocument) {
1644 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1645
        }
1646 627
        $visited = array();
1647 627
        $this->doPersist($document, $visited);
1648 623
    }
1649
1650
    /**
1651
     * Saves a document as part of the current unit of work.
1652
     * This method is internally called during save() cascades as it tracks
1653
     * the already visited documents to prevent infinite recursions.
1654
     *
1655
     * NOTE: This method always considers documents that are not yet known to
1656
     * this UnitOfWork as NEW.
1657
     *
1658
     * @param object $document The document to persist.
1659
     * @param array $visited The already visited documents.
1660
     * @throws \InvalidArgumentException
1661
     * @throws MongoDBException
1662
     */
1663 627
    private function doPersist($document, array &$visited)
1664
    {
1665 627
        $oid = spl_object_hash($document);
1666 627
        if (isset($visited[$oid])) {
1667 24
            return; // Prevent infinite recursion
1668
        }
1669
1670 627
        $visited[$oid] = $document; // Mark visited
1671
1672 627
        $class = $this->dm->getClassMetadata(get_class($document));
1673
1674 627
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1675
        switch ($documentState) {
1676 627
            case self::STATE_MANAGED:
1677
                // Nothing to do, except if policy is "deferred explicit"
1678 53
                if ($class->isChangeTrackingDeferredExplicit()) {
1679
                    $this->scheduleForDirtyCheck($document);
1680
                }
1681 53
                break;
1682 627
            case self::STATE_NEW:
1683 627
                $this->persistNew($class, $document);
1684 625
                break;
1685
1686 2
            case self::STATE_REMOVED:
1687
                // Document becomes managed again
1688 2
                unset($this->documentDeletions[$oid]);
1689
1690 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1691 2
                break;
1692
1693
            case self::STATE_DETACHED:
1694
                throw new \InvalidArgumentException(
1695
                    'Behavior of persist() for a detached document is not yet defined.');
1696
1697
            default:
1698
                throw MongoDBException::invalidDocumentState($documentState);
1699
        }
1700
1701 625
        $this->cascadePersist($document, $visited);
1702 623
    }
1703
1704
    /**
1705
     * Deletes a document as part of the current unit of work.
1706
     *
1707
     * @param object $document The document to remove.
1708
     */
1709 73
    public function remove($document)
1710
    {
1711 73
        $visited = array();
1712 73
        $this->doRemove($document, $visited);
1713 73
    }
1714
1715
    /**
1716
     * Deletes a document as part of the current unit of work.
1717
     *
1718
     * This method is internally called during delete() cascades as it tracks
1719
     * the already visited documents to prevent infinite recursions.
1720
     *
1721
     * @param object $document The document to delete.
1722
     * @param array $visited The map of the already visited documents.
1723
     * @throws MongoDBException
1724
     */
1725 73
    private function doRemove($document, array &$visited)
1726
    {
1727 73
        $oid = spl_object_hash($document);
1728 73
        if (isset($visited[$oid])) {
1729 1
            return; // Prevent infinite recursion
1730
        }
1731
1732 73
        $visited[$oid] = $document; // mark visited
1733
1734
        /* Cascade first, because scheduleForDelete() removes the entity from
1735
         * the identity map, which can cause problems when a lazy Proxy has to
1736
         * be initialized for the cascade operation.
1737
         */
1738 73
        $this->cascadeRemove($document, $visited);
1739
1740 73
        $class = $this->dm->getClassMetadata(get_class($document));
1741 73
        $documentState = $this->getDocumentState($document);
1742
        switch ($documentState) {
1743 73
            case self::STATE_NEW:
1744 73
            case self::STATE_REMOVED:
1745
                // nothing to do
1746 1
                break;
1747 73
            case self::STATE_MANAGED:
1748 73
                $this->lifecycleEventManager->preRemove($class, $document);
1749 73
                $this->scheduleForDelete($document);
1750 73
                break;
1751
            case self::STATE_DETACHED:
1752
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1753
            default:
1754
                throw MongoDBException::invalidDocumentState($documentState);
1755
        }
1756 73
    }
1757
1758
    /**
1759
     * Merges the state of the given detached document into this UnitOfWork.
1760
     *
1761
     * @param object $document
1762
     * @return object The managed copy of the document.
1763
     */
1764 15
    public function merge($document)
1765
    {
1766 15
        $visited = array();
1767
1768 15
        return $this->doMerge($document, $visited);
1769
    }
1770
1771
    /**
1772
     * Executes a merge operation on a document.
1773
     *
1774
     * @param object      $document
1775
     * @param array       $visited
1776
     * @param object|null $prevManagedCopy
1777
     * @param array|null  $assoc
1778
     *
1779
     * @return object The managed copy of the document.
1780
     *
1781
     * @throws InvalidArgumentException If the entity instance is NEW.
1782
     * @throws LockException If the document uses optimistic locking through a
1783
     *                       version attribute and the version check against the
1784
     *                       managed copy fails.
1785
     */
1786 15
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1787
    {
1788 15
        $oid = spl_object_hash($document);
1789
1790 15
        if (isset($visited[$oid])) {
1791 1
            return $visited[$oid]; // Prevent infinite recursion
1792
        }
1793
1794 15
        $visited[$oid] = $document; // mark visited
1795
1796 15
        $class = $this->dm->getClassMetadata(get_class($document));
1797
1798
        /* First we assume DETACHED, although it can still be NEW but we can
1799
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1800
         * an identity, we need to fetch it from the DB anyway in order to
1801
         * merge. MANAGED documents are ignored by the merge operation.
1802
         */
1803 15
        $managedCopy = $document;
1804
1805 15
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1806 15
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1807
                $document->__load();
1808
            }
1809
1810 15
            $identifier = $class->getIdentifier();
1811
            // We always have one element in the identifier array but it might be null
1812 15
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1813 15
            $managedCopy = null;
1814
1815
            // Try to fetch document from the database
1816 15
            if (! $class->isEmbeddedDocument && $id !== null) {
1817 12
                $managedCopy = $this->dm->find($class->name, $id);
1818
1819
                // Managed copy may be removed in which case we can't merge
1820 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1821
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1822
                }
1823
1824 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...
1825
                    $managedCopy->__load();
1826
                }
1827
            }
1828
1829 15
            if ($managedCopy === null) {
1830
                // Create a new managed instance
1831 7
                $managedCopy = $class->newInstance();
1832 7
                if ($id !== null) {
1833 3
                    $class->setIdentifierValue($managedCopy, $id);
1834
                }
1835 7
                $this->persistNew($class, $managedCopy);
1836
            }
1837
1838 15
            if ($class->isVersioned) {
1839
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1840
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1841
1842
                // Throw exception if versions don't match
1843
                if ($managedCopyVersion != $documentVersion) {
1844
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1845
                }
1846
            }
1847
1848
            // Merge state of $document into existing (managed) document
1849 15
            foreach ($class->reflClass->getProperties() as $prop) {
1850 15
                $name = $prop->name;
1851 15
                $prop->setAccessible(true);
1852 15
                if ( ! isset($class->associationMappings[$name])) {
1853 15
                    if ( ! $class->isIdentifier($name)) {
1854 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1855
                    }
1856
                } else {
1857 15
                    $assoc2 = $class->associationMappings[$name];
1858
1859 15
                    if ($assoc2['type'] === 'one') {
1860 7
                        $other = $prop->getValue($document);
1861
1862 7
                        if ($other === null) {
1863 2
                            $prop->setValue($managedCopy, null);
1864 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...
1865
                            // Do not merge fields marked lazy that have not been fetched
1866 1
                            continue;
1867 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1868
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1869
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1870
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1871
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1872
                                $relatedId = $targetClass->getIdentifierObject($other);
1873
1874
                                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...
1875
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1876
                                } else {
1877
                                    $other = $this
1878
                                        ->dm
1879
                                        ->getProxyFactory()
1880
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1881
                                    $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...
1882
                                }
1883
                            }
1884
1885 6
                            $prop->setValue($managedCopy, $other);
1886
                        }
1887
                    } else {
1888 12
                        $mergeCol = $prop->getValue($document);
1889
1890 12
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1891
                            /* Do not merge fields marked lazy that have not
1892
                             * been fetched. Keep the lazy persistent collection
1893
                             * of the managed copy.
1894
                             */
1895 3
                            continue;
1896
                        }
1897
1898 12
                        $managedCol = $prop->getValue($managedCopy);
1899
1900 12
                        if ( ! $managedCol) {
1901 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1902 3
                            $managedCol->setOwner($managedCopy, $assoc2);
1903 3
                            $prop->setValue($managedCopy, $managedCol);
1904 3
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1905
                        }
1906
1907
                        /* Note: do not process association's target documents.
1908
                         * They will be handled during the cascade. Initialize
1909
                         * and, if necessary, clear $managedCol for now.
1910
                         */
1911 12
                        if ($assoc2['isCascadeMerge']) {
1912 12
                            $managedCol->initialize();
1913
1914
                            // If $managedCol differs from the merged collection, clear and set dirty
1915 12
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1916 3
                                $managedCol->unwrap()->clear();
1917 3
                                $managedCol->setDirty(true);
1918
1919 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1920
                                    $this->scheduleForDirtyCheck($managedCopy);
1921
                                }
1922
                            }
1923
                        }
1924
                    }
1925
                }
1926
1927 15
                if ($class->isChangeTrackingNotify()) {
1928
                    // Just treat all properties as changed, there is no other choice.
1929 15
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1930
                }
1931
            }
1932
1933 15
            if ($class->isChangeTrackingDeferredExplicit()) {
1934
                $this->scheduleForDirtyCheck($document);
1935
            }
1936
        }
1937
1938 15
        if ($prevManagedCopy !== null) {
1939 8
            $assocField = $assoc['fieldName'];
1940 8
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1941
1942 8
            if ($assoc['type'] === 'one') {
1943 4
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1944
            } else {
1945 6
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1946
1947 6
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1948 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1949
                }
1950
            }
1951
        }
1952
1953
        // Mark the managed copy visited as well
1954 15
        $visited[spl_object_hash($managedCopy)] = true;
1955
1956 15
        $this->cascadeMerge($document, $managedCopy, $visited);
1957
1958 15
        return $managedCopy;
1959
    }
1960
1961
    /**
1962
     * Detaches a document from the persistence management. It's persistence will
1963
     * no longer be managed by Doctrine.
1964
     *
1965
     * @param object $document The document to detach.
1966
     */
1967 11
    public function detach($document)
1968
    {
1969 11
        $visited = array();
1970 11
        $this->doDetach($document, $visited);
1971 11
    }
1972
1973
    /**
1974
     * Executes a detach operation on the given document.
1975
     *
1976
     * @param object $document
1977
     * @param array $visited
1978
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1979
     */
1980 16
    private function doDetach($document, array &$visited)
1981
    {
1982 16
        $oid = spl_object_hash($document);
1983 16
        if (isset($visited[$oid])) {
1984 4
            return; // Prevent infinite recursion
1985
        }
1986
1987 16
        $visited[$oid] = $document; // mark visited
1988
1989 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1990 16
            case self::STATE_MANAGED:
1991 16
                $this->removeFromIdentityMap($document);
1992 16
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1993 16
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1994 16
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1995 16
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1996 16
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
1997 16
                break;
1998 4
            case self::STATE_NEW:
1999 4
            case self::STATE_DETACHED:
2000 4
                return;
2001
        }
2002
2003 16
        $this->cascadeDetach($document, $visited);
2004 16
    }
2005
2006
    /**
2007
     * Refreshes the state of the given document from the database, overwriting
2008
     * any local, unpersisted changes.
2009
     *
2010
     * @param object $document The document to refresh.
2011
     * @throws \InvalidArgumentException If the document is not MANAGED.
2012
     */
2013 21
    public function refresh($document)
2014
    {
2015 21
        $visited = array();
2016 21
        $this->doRefresh($document, $visited);
2017 20
    }
2018
2019
    /**
2020
     * Executes a refresh operation on a document.
2021
     *
2022
     * @param object $document The document to refresh.
2023
     * @param array $visited The already visited documents during cascades.
2024
     * @throws \InvalidArgumentException If the document is not MANAGED.
2025
     */
2026 21
    private function doRefresh($document, array &$visited)
2027
    {
2028 21
        $oid = spl_object_hash($document);
2029 21
        if (isset($visited[$oid])) {
2030
            return; // Prevent infinite recursion
2031
        }
2032
2033 21
        $visited[$oid] = $document; // mark visited
2034
2035 21
        $class = $this->dm->getClassMetadata(get_class($document));
2036
2037 21
        if ( ! $class->isEmbeddedDocument) {
2038 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2039 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2040 20
                $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...
2041
            } else {
2042 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2043
            }
2044
        }
2045
2046 20
        $this->cascadeRefresh($document, $visited);
2047 20
    }
2048
2049
    /**
2050
     * Cascades a refresh operation to associated documents.
2051
     *
2052
     * @param object $document
2053
     * @param array $visited
2054
     */
2055 20
    private function cascadeRefresh($document, array &$visited)
2056
    {
2057 20
        $class = $this->dm->getClassMetadata(get_class($document));
2058
2059 20
        $associationMappings = array_filter(
2060 20
            $class->associationMappings,
2061
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2062
        );
2063
2064 20
        foreach ($associationMappings as $mapping) {
2065 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2066 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2067 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2068
                    // Unwrap so that foreach() does not initialize
2069 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2070
                }
2071 15
                foreach ($relatedDocuments as $relatedDocument) {
2072 15
                    $this->doRefresh($relatedDocument, $visited);
2073
                }
2074 10
            } elseif ($relatedDocuments !== null) {
2075 15
                $this->doRefresh($relatedDocuments, $visited);
2076
            }
2077
        }
2078 20
    }
2079
2080
    /**
2081
     * Cascades a detach operation to associated documents.
2082
     *
2083
     * @param object $document
2084
     * @param array $visited
2085
     */
2086 16 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2087
    {
2088 16
        $class = $this->dm->getClassMetadata(get_class($document));
2089 16
        foreach ($class->fieldMappings as $mapping) {
2090 16
            if ( ! $mapping['isCascadeDetach']) {
2091 16
                continue;
2092
            }
2093 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2094 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2095 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2096
                    // Unwrap so that foreach() does not initialize
2097 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2098
                }
2099 11
                foreach ($relatedDocuments as $relatedDocument) {
2100 11
                    $this->doDetach($relatedDocument, $visited);
2101
                }
2102 11
            } elseif ($relatedDocuments !== null) {
2103 11
                $this->doDetach($relatedDocuments, $visited);
2104
            }
2105
        }
2106 16
    }
2107
    /**
2108
     * Cascades a merge operation to associated documents.
2109
     *
2110
     * @param object $document
2111
     * @param object $managedCopy
2112
     * @param array $visited
2113
     */
2114 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2115
    {
2116 15
        $class = $this->dm->getClassMetadata(get_class($document));
2117
2118 15
        $associationMappings = array_filter(
2119 15
            $class->associationMappings,
2120
            function ($assoc) { return $assoc['isCascadeMerge']; }
2121
        );
2122
2123 15
        foreach ($associationMappings as $assoc) {
2124 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2125
2126 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2127 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2128
                    // Collections are the same, so there is nothing to do
2129 1
                    continue;
2130
                }
2131
2132 10
                foreach ($relatedDocuments as $relatedDocument) {
2133 10
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2134
                }
2135 7
            } elseif ($relatedDocuments !== null) {
2136 14
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2137
            }
2138
        }
2139 15
    }
2140
2141
    /**
2142
     * Cascades the save operation to associated documents.
2143
     *
2144
     * @param object $document
2145
     * @param array $visited
2146
     */
2147 625
    private function cascadePersist($document, array &$visited)
2148
    {
2149 625
        $class = $this->dm->getClassMetadata(get_class($document));
2150
2151 625
        $associationMappings = array_filter(
2152 625
            $class->associationMappings,
2153
            function ($assoc) { return $assoc['isCascadePersist']; }
2154
        );
2155
2156 625
        foreach ($associationMappings as $fieldName => $mapping) {
2157 434
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2158
2159 434
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2160 364
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2161 17
                    if ($relatedDocuments->getOwner() !== $document) {
2162 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2163
                    }
2164
                    // Unwrap so that foreach() does not initialize
2165 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2166
                }
2167
2168 364
                $count = 0;
2169 364
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2170 204
                    if ( ! empty($mapping['embedded'])) {
2171 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2172 126
                        if ($knownParent && $knownParent !== $document) {
2173 4
                            $relatedDocument = clone $relatedDocument;
2174 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2175
                        }
2176 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2177 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2178
                    }
2179 364
                    $this->doPersist($relatedDocument, $visited);
2180
                }
2181 343
            } elseif ($relatedDocuments !== null) {
2182 133
                if ( ! empty($mapping['embedded'])) {
2183 76
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2184 76
                    if ($knownParent && $knownParent !== $document) {
2185 5
                        $relatedDocuments = clone $relatedDocuments;
2186 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2187
                    }
2188 76
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2189
                }
2190 434
                $this->doPersist($relatedDocuments, $visited);
2191
            }
2192
        }
2193 623
    }
2194
2195
    /**
2196
     * Cascades the delete operation to associated documents.
2197
     *
2198
     * @param object $document
2199
     * @param array $visited
2200
     */
2201 73 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...
2202
    {
2203 73
        $class = $this->dm->getClassMetadata(get_class($document));
2204 73
        foreach ($class->fieldMappings as $mapping) {
2205 73
            if ( ! $mapping['isCascadeRemove']) {
2206 72
                continue;
2207
            }
2208 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...
2209 2
                $document->__load();
2210
            }
2211
2212 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2213 36
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2214
                // If its a PersistentCollection initialization is intended! No unwrap!
2215 25
                foreach ($relatedDocuments as $relatedDocument) {
2216 25
                    $this->doRemove($relatedDocument, $visited);
2217
                }
2218 24
            } elseif ($relatedDocuments !== null) {
2219 36
                $this->doRemove($relatedDocuments, $visited);
2220
            }
2221
        }
2222 73
    }
2223
2224
    /**
2225
     * Acquire a lock on the given document.
2226
     *
2227
     * @param object $document
2228
     * @param int $lockMode
2229
     * @param int $lockVersion
2230
     * @throws LockException
2231
     * @throws \InvalidArgumentException
2232
     */
2233 9
    public function lock($document, $lockMode, $lockVersion = null)
2234
    {
2235 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2236 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2237
        }
2238
2239 8
        $documentName = get_class($document);
2240 8
        $class = $this->dm->getClassMetadata($documentName);
2241
2242 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2243 3
            if ( ! $class->isVersioned) {
2244 1
                throw LockException::notVersioned($documentName);
2245
            }
2246
2247 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...
2248 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2249 2
                if ($documentVersion != $lockVersion) {
2250 2
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2251
                }
2252
            }
2253 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2254 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2255
        }
2256 6
    }
2257
2258
    /**
2259
     * Releases a lock on the given document.
2260
     *
2261
     * @param object $document
2262
     * @throws \InvalidArgumentException
2263
     */
2264 1
    public function unlock($document)
2265
    {
2266 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2267
            throw new \InvalidArgumentException("Document is not MANAGED.");
2268
        }
2269 1
        $documentName = get_class($document);
2270 1
        $this->getDocumentPersister($documentName)->unlock($document);
2271 1
    }
2272
2273
    /**
2274
     * Clears the UnitOfWork.
2275
     *
2276
     * @param string|null $documentName if given, only documents of this type will get detached.
2277
     */
2278 410
    public function clear($documentName = null)
2279
    {
2280 410
        if ($documentName === null) {
2281 402
            $this->identityMap =
2282 402
            $this->documentIdentifiers =
2283 402
            $this->originalDocumentData =
2284 402
            $this->documentChangeSets =
2285 402
            $this->documentStates =
2286 402
            $this->scheduledForDirtyCheck =
2287 402
            $this->documentInsertions =
2288 402
            $this->documentUpserts =
2289 402
            $this->documentUpdates =
2290 402
            $this->documentDeletions =
2291 402
            $this->collectionUpdates =
2292 402
            $this->collectionDeletions =
2293 402
            $this->parentAssociations =
2294 402
            $this->embeddedDocumentsRegistry =
2295 402
            $this->orphanRemovals =
2296 402
            $this->hasScheduledCollections = array();
2297
        } else {
2298 8
            $visited = array();
2299 8
            foreach ($this->identityMap as $className => $documents) {
2300 8
                if ($className === $documentName) {
2301 5
                    foreach ($documents as $document) {
2302 8
                        $this->doDetach($document, $visited);
2303
                    }
2304
                }
2305
            }
2306
        }
2307
2308 410 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...
2309
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2310
        }
2311 410
    }
2312
2313
    /**
2314
     * INTERNAL:
2315
     * Schedules an embedded document for removal. The remove() operation will be
2316
     * invoked on that document at the beginning of the next commit of this
2317
     * UnitOfWork.
2318
     *
2319
     * @ignore
2320
     * @param object $document
2321
     */
2322 50
    public function scheduleOrphanRemoval($document)
2323
    {
2324 50
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2325 50
    }
2326
2327
    /**
2328
     * INTERNAL:
2329
     * Unschedules an embedded or referenced object for removal.
2330
     *
2331
     * @ignore
2332
     * @param object $document
2333
     */
2334 112
    public function unscheduleOrphanRemoval($document)
2335
    {
2336 112
        $oid = spl_object_hash($document);
2337 112
        if (isset($this->orphanRemovals[$oid])) {
2338 1
            unset($this->orphanRemovals[$oid]);
2339
        }
2340 112
    }
2341
2342
    /**
2343
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2344
     *  1) sets owner if it was cloned
2345
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2346
     *  3) NOP if state is OK
2347
     * Returned collection should be used from now on (only important with 2nd point)
2348
     *
2349
     * @param PersistentCollectionInterface $coll
2350
     * @param object $document
2351
     * @param ClassMetadata $class
2352
     * @param string $propName
2353
     * @return PersistentCollectionInterface
2354
     */
2355 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2356
    {
2357 8
        $owner = $coll->getOwner();
2358 8
        if ($owner === null) { // cloned
2359 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2360 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2361 2
            if ( ! $coll->isInitialized()) {
2362 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2363
            }
2364 2
            $newValue = clone $coll;
2365 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2366 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2367 2
            if ($this->isScheduledForUpdate($document)) {
2368
                // @todo following line should be superfluous once collections are stored in change sets
2369
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2370
            }
2371 2
            return $newValue;
2372
        }
2373 6
        return $coll;
2374
    }
2375
2376
    /**
2377
     * INTERNAL:
2378
     * Schedules a complete collection for removal when this UnitOfWork commits.
2379
     *
2380
     * @param PersistentCollectionInterface $coll
2381
     */
2382 42
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2383
    {
2384 42
        $oid = spl_object_hash($coll);
2385 42
        unset($this->collectionUpdates[$oid]);
2386 42
        if ( ! isset($this->collectionDeletions[$oid])) {
2387 42
            $this->collectionDeletions[$oid] = $coll;
2388 42
            $this->scheduleCollectionOwner($coll);
2389
        }
2390 42
    }
2391
2392
    /**
2393
     * Checks whether a PersistentCollection is scheduled for deletion.
2394
     *
2395
     * @param PersistentCollectionInterface $coll
2396
     * @return boolean
2397
     */
2398 218
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2399
    {
2400 218
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2401
    }
2402
2403
    /**
2404
     * INTERNAL:
2405
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2406
     *
2407
     * @param PersistentCollectionInterface $coll
2408
     */
2409 222 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...
2410
    {
2411 222
        $oid = spl_object_hash($coll);
2412 222
        if (isset($this->collectionDeletions[$oid])) {
2413 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2414 12
            unset($this->collectionDeletions[$oid]);
2415 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2416
        }
2417 222
    }
2418
2419
    /**
2420
     * INTERNAL:
2421
     * Schedules a collection for update when this UnitOfWork commits.
2422
     *
2423
     * @param PersistentCollectionInterface $coll
2424
     */
2425 243
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2426
    {
2427 243
        $mapping = $coll->getMapping();
2428 243
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2429
            /* There is no need to $unset collection if it will be $set later
2430
             * This is NOP if collection is not scheduled for deletion
2431
             */
2432 41
            $this->unscheduleCollectionDeletion($coll);
2433
        }
2434 243
        $oid = spl_object_hash($coll);
2435 243
        if ( ! isset($this->collectionUpdates[$oid])) {
2436 243
            $this->collectionUpdates[$oid] = $coll;
2437 243
            $this->scheduleCollectionOwner($coll);
2438
        }
2439 243
    }
2440
2441
    /**
2442
     * INTERNAL:
2443
     * Unschedules a collection from being updated when this UnitOfWork commits.
2444
     *
2445
     * @param PersistentCollectionInterface $coll
2446
     */
2447 222 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...
2448
    {
2449 222
        $oid = spl_object_hash($coll);
2450 222
        if (isset($this->collectionUpdates[$oid])) {
2451 212
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2452 212
            unset($this->collectionUpdates[$oid]);
2453 212
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2454
        }
2455 222
    }
2456
2457
    /**
2458
     * Checks whether a PersistentCollection is scheduled for update.
2459
     *
2460
     * @param PersistentCollectionInterface $coll
2461
     * @return boolean
2462
     */
2463 131
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2464
    {
2465 131
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2466
    }
2467
2468
    /**
2469
     * INTERNAL:
2470
     * Gets PersistentCollections that have been visited during computing change
2471
     * set of $document
2472
     *
2473
     * @param object $document
2474
     * @return PersistentCollectionInterface[]
2475
     */
2476 584
    public function getVisitedCollections($document)
2477
    {
2478 584
        $oid = spl_object_hash($document);
2479 584
        return isset($this->visitedCollections[$oid])
2480 246
                ? $this->visitedCollections[$oid]
2481 584
                : array();
2482
    }
2483
2484
    /**
2485
     * INTERNAL:
2486
     * Gets PersistentCollections that are scheduled to update and related to $document
2487
     *
2488
     * @param object $document
2489
     * @return array
2490
     */
2491 584
    public function getScheduledCollections($document)
2492
    {
2493 584
        $oid = spl_object_hash($document);
2494 584
        return isset($this->hasScheduledCollections[$oid])
2495 244
                ? $this->hasScheduledCollections[$oid]
2496 584
                : array();
2497
    }
2498
2499
    /**
2500
     * Checks whether the document is related to a PersistentCollection
2501
     * scheduled for update or deletion.
2502
     *
2503
     * @param object $document
2504
     * @return boolean
2505
     */
2506 52
    public function hasScheduledCollections($document)
2507
    {
2508 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2509
    }
2510
2511
    /**
2512
     * Marks the PersistentCollection's top-level owner as having a relation to
2513
     * a collection scheduled for update or deletion.
2514
     *
2515
     * If the owner is not scheduled for any lifecycle action, it will be
2516
     * scheduled for update to ensure that versioning takes place if necessary.
2517
     *
2518
     * If the collection is nested within atomic collection, it is immediately
2519
     * unscheduled and atomic one is scheduled for update instead. This makes
2520
     * calculating update data way easier.
2521
     *
2522
     * @param PersistentCollectionInterface $coll
2523
     */
2524 245
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2525
    {
2526 245
        $document = $this->getOwningDocument($coll->getOwner());
2527 245
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2528
2529 245
        if ($document !== $coll->getOwner()) {
2530 25
            $parent = $coll->getOwner();
2531 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2532 25
                list($mapping, $parent, ) = $parentAssoc;
2533
            }
2534 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2535 8
                $class = $this->dm->getClassMetadata(get_class($document));
2536 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...
2537 8
                $this->scheduleCollectionUpdate($atomicCollection);
2538 8
                $this->unscheduleCollectionDeletion($coll);
2539 8
                $this->unscheduleCollectionUpdate($coll);
2540
            }
2541
        }
2542
2543 245
        if ( ! $this->isDocumentScheduled($document)) {
2544 49
            $this->scheduleForUpdate($document);
2545
        }
2546 245
    }
2547
2548
    /**
2549
     * Get the top-most owning document of a given document
2550
     *
2551
     * If a top-level document is provided, that same document will be returned.
2552
     * For an embedded document, we will walk through parent associations until
2553
     * we find a top-level document.
2554
     *
2555
     * @param object $document
2556
     * @throws \UnexpectedValueException when a top-level document could not be found
2557
     * @return object
2558
     */
2559 247
    public function getOwningDocument($document)
2560
    {
2561 247
        $class = $this->dm->getClassMetadata(get_class($document));
2562 247
        while ($class->isEmbeddedDocument) {
2563 40
            $parentAssociation = $this->getParentAssociation($document);
2564
2565 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...
2566
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2567
            }
2568
2569 40
            list(, $document, ) = $parentAssociation;
2570 40
            $class = $this->dm->getClassMetadata(get_class($document));
2571
        }
2572
2573 247
        return $document;
2574
    }
2575
2576
    /**
2577
     * Gets the class name for an association (embed or reference) with respect
2578
     * to any discriminator value.
2579
     *
2580
     * @param array      $mapping Field mapping for the association
2581
     * @param array|null $data    Data for the embedded document or reference
2582
     * @return string Class name.
2583
     */
2584 221
    public function getClassNameForAssociation(array $mapping, $data)
2585
    {
2586 221
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2587
2588 221
        $discriminatorValue = null;
2589 221
        if (isset($discriminatorField, $data[$discriminatorField])) {
2590 21
            $discriminatorValue = $data[$discriminatorField];
2591 201
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2592
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2593
        }
2594
2595 221
        if ($discriminatorValue !== null) {
2596 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2597 10
                ? $mapping['discriminatorMap'][$discriminatorValue]
2598 21
                : $discriminatorValue;
2599
        }
2600
2601 201
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2602
2603 201 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...
2604 15
            $discriminatorValue = $data[$class->discriminatorField];
2605 186
        } elseif ($class->defaultDiscriminatorValue !== null) {
2606 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2607
        }
2608
2609 201
        if ($discriminatorValue !== null) {
2610 16
            return isset($class->discriminatorMap[$discriminatorValue])
2611 14
                ? $class->discriminatorMap[$discriminatorValue]
2612 16
                : $discriminatorValue;
2613
        }
2614
2615 185
        return $mapping['targetDocument'];
2616
    }
2617
2618
    /**
2619
     * INTERNAL:
2620
     * Creates a document. Used for reconstitution of documents during hydration.
2621
     *
2622
     * @ignore
2623
     * @param string $className The name of the document class.
2624
     * @param array $data The data for the document.
2625
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2626
     * @param object $document The document to be hydrated into in case of creation
2627
     * @return object The document instance.
2628
     * @internal Highly performance-sensitive method.
2629
     */
2630 412
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2631
    {
2632 412
        $class = $this->dm->getClassMetadata($className);
2633
2634
        // @TODO figure out how to remove this
2635 412
        $discriminatorValue = null;
2636 412 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...
2637 19
            $discriminatorValue = $data[$class->discriminatorField];
2638
        } elseif (isset($class->defaultDiscriminatorValue)) {
2639 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2640
        }
2641
2642 412
        if ($discriminatorValue !== null) {
2643 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2644 18
                ? $class->discriminatorMap[$discriminatorValue]
2645 20
                : $discriminatorValue;
2646
2647 20
            $class = $this->dm->getClassMetadata($className);
2648
2649 20
            unset($data[$class->discriminatorField]);
2650
        }
2651
        
2652 412
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2653 2
            $document = $class->newInstance();
2654 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2655 2
            return $document;
2656
        }
2657
2658 411
        $isManagedObject = false;
2659 411
        if (! $class->isAggregationResultDocument) {
2660 411
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2661 411
            $serializedId = serialize($id);
2662 411
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2663
        }
2664
2665 411
        if ($isManagedObject) {
2666 104
            $document = $this->identityMap[$class->name][$serializedId];
0 ignored issues
show
Bug introduced by
The variable $serializedId does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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