Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

UnitOfWork::getDocumentActualData()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 19
cts 19
cp 1
rs 4.909
c 0
b 0
f 0
cc 9
eloc 20
nc 5
nop 1
crap 9
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\ODM\MongoDB\Hydrator\HydratorFactory;
28
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
29
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
30
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
31
use Doctrine\ODM\MongoDB\Proxy\Proxy;
32
use Doctrine\ODM\MongoDB\Query\Query;
33
use Doctrine\ODM\MongoDB\Types\Type;
34
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
35
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
36
37
/**
38
 * The UnitOfWork is responsible for tracking changes to objects during an
39
 * "object-level" transaction and for writing out changes to the database
40
 * in the correct order.
41
 *
42
 * @since       1.0
43
 */
44
class UnitOfWork implements PropertyChangedListener
45
{
46
    /**
47
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
48
     */
49
    const STATE_MANAGED = 1;
50
51
    /**
52
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
53
     * and is not (yet) managed by a DocumentManager.
54
     */
55
    const STATE_NEW = 2;
56
57
    /**
58
     * A detached document is an instance with a persistent identity that is not
59
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
60
     */
61
    const STATE_DETACHED = 3;
62
63
    /**
64
     * A removed document instance is an instance with a persistent identity,
65
     * associated with a DocumentManager, whose persistent state has been
66
     * deleted (or is scheduled for deletion).
67
     */
68
    const STATE_REMOVED = 4;
69
70
    /**
71
     * The identity map holds references to all managed documents.
72
     *
73
     * Documents are grouped by their class name, and then indexed by the
74
     * serialized string of their database identifier field or, if the class
75
     * has no identifier, the SPL object hash. Serializing the identifier allows
76
     * differentiation of values that may be equal (via type juggling) but not
77
     * identical.
78
     *
79
     * Since all classes in a hierarchy must share the same identifier set,
80
     * we always take the root class name of the hierarchy.
81
     *
82
     * @var array
83
     */
84
    private $identityMap = array();
85
86
    /**
87
     * Map of all identifiers of managed documents.
88
     * Keys are object ids (spl_object_hash).
89
     *
90
     * @var array
91
     */
92
    private $documentIdentifiers = array();
93
94
    /**
95
     * Map of the original document data of managed documents.
96
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
97
     * at commit time.
98
     *
99
     * @var array
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     */
104
    private $originalDocumentData = array();
105
106
    /**
107
     * Map of document changes. Keys are object ids (spl_object_hash).
108
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
109
     *
110
     * @var array
111
     */
112
    private $documentChangeSets = array();
113
114
    /**
115
     * The (cached) states of any known documents.
116
     * Keys are object ids (spl_object_hash).
117
     *
118
     * @var array
119
     */
120
    private $documentStates = array();
121
122
    /**
123
     * Map of documents that are scheduled for dirty checking at commit time.
124
     *
125
     * Documents are grouped by their class name, and then indexed by their SPL
126
     * object hash. This is only used for documents with a change tracking
127
     * policy of DEFERRED_EXPLICIT.
128
     *
129
     * @var array
130
     * @todo rename: scheduledForSynchronization
131
     */
132
    private $scheduledForDirtyCheck = array();
133
134
    /**
135
     * A list of all pending document insertions.
136
     *
137
     * @var array
138
     */
139
    private $documentInsertions = array();
140
141
    /**
142
     * A list of all pending document updates.
143
     *
144
     * @var array
145
     */
146
    private $documentUpdates = array();
147
148
    /**
149
     * A list of all pending document upserts.
150
     *
151
     * @var array
152
     */
153
    private $documentUpserts = array();
154
155
    /**
156
     * A list of all pending document deletions.
157
     *
158
     * @var array
159
     */
160
    private $documentDeletions = array();
161
162
    /**
163
     * All pending collection deletions.
164
     *
165
     * @var array
166
     */
167
    private $collectionDeletions = array();
168
169
    /**
170
     * All pending collection updates.
171
     *
172
     * @var array
173
     */
174
    private $collectionUpdates = array();
175
176
    /**
177
     * A list of documents related to collections scheduled for update or deletion
178
     *
179
     * @var array
180
     */
181
    private $hasScheduledCollections = array();
182
183
    /**
184
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
185
     * At the end of the UnitOfWork all these collections will make new snapshots
186
     * of their data.
187
     *
188
     * @var array
189
     */
190
    private $visitedCollections = array();
191
192
    /**
193
     * The DocumentManager that "owns" this UnitOfWork instance.
194
     *
195
     * @var DocumentManager
196
     */
197
    private $dm;
198
199
    /**
200
     * The EventManager used for dispatching events.
201
     *
202
     * @var EventManager
203
     */
204
    private $evm;
205
206
    /**
207
     * Additional documents that are scheduled for removal.
208
     *
209
     * @var array
210
     */
211
    private $orphanRemovals = array();
212
213
    /**
214
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
215
     *
216
     * @var HydratorFactory
217
     */
218
    private $hydratorFactory;
219
220
    /**
221
     * The document persister instances used to persist document instances.
222
     *
223
     * @var array
224
     */
225
    private $persisters = array();
226
227
    /**
228
     * The collection persister instance used to persist changes to collections.
229
     *
230
     * @var Persisters\CollectionPersister
231
     */
232
    private $collectionPersister;
233
234
    /**
235
     * The persistence builder instance used in DocumentPersisters.
236
     *
237
     * @var PersistenceBuilder
238
     */
239
    private $persistenceBuilder;
240
241
    /**
242
     * Array of parent associations between embedded documents.
243
     *
244
     * @var array
245
     */
246
    private $parentAssociations = array();
247
248
    /**
249
     * @var LifecycleEventManager
250
     */
251
    private $lifecycleEventManager;
252
253
    /**
254
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
255
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
256
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
257
     *
258
     * @var array
259
     */
260
    private $embeddedDocumentsRegistry = array();
261
262
    /**
263
     * @var int
264
     */
265
    private $commitsInProgress = 0;
266
267
    /**
268
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
269
     *
270
     * @param DocumentManager $dm
271
     * @param EventManager $evm
272
     * @param HydratorFactory $hydratorFactory
273
     */
274 1638
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
275
    {
276 1638
        $this->dm = $dm;
277 1638
        $this->evm = $evm;
278 1638
        $this->hydratorFactory = $hydratorFactory;
279 1638
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
280 1638
    }
281
282
    /**
283
     * Factory for returning new PersistenceBuilder instances used for preparing data into
284
     * queries for insert persistence.
285
     *
286
     * @return PersistenceBuilder $pb
287
     */
288 1086
    public function getPersistenceBuilder()
289
    {
290 1086
        if ( ! $this->persistenceBuilder) {
291 1086
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
292
        }
293 1086
        return $this->persistenceBuilder;
294
    }
295
296
    /**
297
     * Sets the parent association for a given embedded document.
298
     *
299
     * @param object $document
300
     * @param array $mapping
301
     * @param object $parent
302
     * @param string $propertyPath
303
     */
304 177
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
305
    {
306 177
        $oid = spl_object_hash($document);
307 177
        $this->embeddedDocumentsRegistry[$oid] = $document;
308 177
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
309 177
    }
310
311
    /**
312
     * Gets the parent association for a given embedded document.
313
     *
314
     *     <code>
315
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
316
     *     </code>
317
     *
318
     * @param object $document
319
     * @return array $association
320
     */
321 203
    public function getParentAssociation($document)
322
    {
323 203
        $oid = spl_object_hash($document);
324 203
        if ( ! isset($this->parentAssociations[$oid])) {
325 197
            return null;
326
        }
327 152
        return $this->parentAssociations[$oid];
328
    }
329
330
    /**
331
     * Get the document persister instance for the given document name
332
     *
333
     * @param string $documentName
334
     * @return Persisters\DocumentPersister
335
     */
336 1084
    public function getDocumentPersister($documentName)
337
    {
338 1084
        if ( ! isset($this->persisters[$documentName])) {
339 1071
            $class = $this->dm->getClassMetadata($documentName);
340 1071
            $pb = $this->getPersistenceBuilder();
341 1071
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
342
        }
343 1084
        return $this->persisters[$documentName];
344
    }
345
346
    /**
347
     * Get the collection persister instance.
348
     *
349
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
350
     */
351 1084
    public function getCollectionPersister()
352
    {
353 1084
        if ( ! isset($this->collectionPersister)) {
354 1084
            $pb = $this->getPersistenceBuilder();
355 1084
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
356
        }
357 1084
        return $this->collectionPersister;
358
    }
359
360
    /**
361
     * Set the document persister instance to use for the given document name
362
     *
363
     * @param string $documentName
364
     * @param Persisters\DocumentPersister $persister
365
     */
366 13
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
367
    {
368 13
        $this->persisters[$documentName] = $persister;
369 13
    }
370
371
    /**
372
     * Commits the UnitOfWork, executing all operations that have been postponed
373
     * up to this point. The state of all managed documents will be synchronized with
374
     * the database.
375
     *
376
     * The operations are executed in the following order:
377
     *
378
     * 1) All document insertions
379
     * 2) All document updates
380
     * 3) All document deletions
381
     *
382
     * @param object $document
383
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
384
     */
385 577
    public function commit($document = null, array $options = array())
386
    {
387
        // Raise preFlush
388 577
        if ($this->evm->hasListeners(Events::preFlush)) {
389
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
390
        }
391
392
        // Compute changes done since last commit.
393 577
        if ($document === null) {
394 572
            $this->computeChangeSets();
395 13
        } elseif (is_object($document)) {
396 12
            $this->computeSingleDocumentChangeSet($document);
397 1
        } elseif (is_array($document)) {
398 1
            foreach ($document as $object) {
399 1
                $this->computeSingleDocumentChangeSet($object);
400
            }
401
        }
402
403 575
        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...
404 245
            $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...
405 202
            $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...
406 188
            $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...
407 22
            $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...
408 22
            $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...
409 575
            $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...
410
        ) {
411 22
            return; // Nothing to do.
412
        }
413
414 572
        $this->commitsInProgress++;
415 572
        if ($this->commitsInProgress > 1) {
416
            @trigger_error('There is already a commit operation in progress. Calling flush in an event subscriber is deprecated and will be forbidden in 2.0.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
417
        }
418
        try {
419 572
            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...
420 44
                foreach ($this->orphanRemovals as $removal) {
421 44
                    $this->remove($removal);
422
                }
423
            }
424
425
            // Raise onFlush
426 572
            if ($this->evm->hasListeners(Events::onFlush)) {
427 4
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
428
            }
429
430 571
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
431 86
                list($class, $documents) = $classAndDocuments;
432 86
                $this->executeUpserts($class, $documents, $options);
433
            }
434
435 571
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
436 496
                list($class, $documents) = $classAndDocuments;
437 496
                $this->executeInserts($class, $documents, $options);
438
            }
439
440 570
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
441 208
                list($class, $documents) = $classAndDocuments;
442 208
                $this->executeUpdates($class, $documents, $options);
443
            }
444
445 570
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
446 65
                list($class, $documents) = $classAndDocuments;
447 65
                $this->executeDeletions($class, $documents, $options);
448
            }
449
450
            // Raise postFlush
451 570
            if ($this->evm->hasListeners(Events::postFlush)) {
452
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
453
            }
454
455
            // Clear up
456 570
            $this->documentInsertions =
457 570
            $this->documentUpserts =
458 570
            $this->documentUpdates =
459 570
            $this->documentDeletions =
460 570
            $this->documentChangeSets =
461 570
            $this->collectionUpdates =
462 570
            $this->collectionDeletions =
463 570
            $this->visitedCollections =
464 570
            $this->scheduledForDirtyCheck =
465 570
            $this->orphanRemovals =
466 570
            $this->hasScheduledCollections = array();
467 570
        } finally {
468 572
            $this->commitsInProgress--;
469
        }
470 570
    }
471
472
    /**
473
     * Groups a list of scheduled documents by their class.
474
     *
475
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
476
     * @param bool $includeEmbedded
477
     * @return array Tuples of ClassMetadata and a corresponding array of objects
478
     */
479 571
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
480
    {
481 571
        if (empty($documents)) {
482 571
            return array();
483
        }
484 570
        $divided = array();
485 570
        $embeds = array();
486 570
        foreach ($documents as $oid => $d) {
487 570
            $className = get_class($d);
488 570
            if (isset($embeds[$className])) {
489 69
                continue;
490
            }
491 570
            if (isset($divided[$className])) {
492 158
                $divided[$className][1][$oid] = $d;
493 158
                continue;
494
            }
495 570
            $class = $this->dm->getClassMetadata($className);
496 570
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
497 154
                $embeds[$className] = true;
498 154
                continue;
499
            }
500 570
            if (empty($divided[$class->name])) {
501 570
                $divided[$class->name] = array($class, array($oid => $d));
502
            } else {
503 570
                $divided[$class->name][1][$oid] = $d;
504
            }
505
        }
506 570
        return $divided;
507
    }
508
509
    /**
510
     * Compute changesets of all documents scheduled for insertion.
511
     *
512
     * Embedded documents will not be processed.
513
     */
514 579 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...
515
    {
516 579
        foreach ($this->documentInsertions as $document) {
517 507
            $class = $this->dm->getClassMetadata(get_class($document));
518 507
            if ( ! $class->isEmbeddedDocument) {
519 507
                $this->computeChangeSet($class, $document);
520
            }
521
        }
522 578
    }
523
524
    /**
525
     * Compute changesets of all documents scheduled for upsert.
526
     *
527
     * Embedded documents will not be processed.
528
     */
529 578 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...
530
    {
531 578
        foreach ($this->documentUpserts as $document) {
532 85
            $class = $this->dm->getClassMetadata(get_class($document));
533 85
            if ( ! $class->isEmbeddedDocument) {
534 85
                $this->computeChangeSet($class, $document);
535
            }
536
        }
537 578
    }
538
539
    /**
540
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
541
     *
542
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
543
     * 2. Proxies are skipped.
544
     * 3. Only if document is properly managed.
545
     *
546
     * @param  object $document
547
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
548
     * @return void
549
     */
550 13
    private function computeSingleDocumentChangeSet($document)
551
    {
552 13
        $state = $this->getDocumentState($document);
553
554 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
555 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
556
        }
557
558 12
        $class = $this->dm->getClassMetadata(get_class($document));
559
560 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
561 10
            $this->persist($document);
562
        }
563
564
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
565 12
        $this->computeScheduleInsertsChangeSets();
566 12
        $this->computeScheduleUpsertsChangeSets();
567
568
        // Ignore uninitialized proxy objects
569 12
        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...
570
            return;
571
        }
572
573
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
574 12
        $oid = spl_object_hash($document);
575
576 12 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...
577 12
            && ! isset($this->documentUpserts[$oid])
578 12
            && ! isset($this->documentDeletions[$oid])
579 12
            && isset($this->documentStates[$oid])
580
        ) {
581 7
            $this->computeChangeSet($class, $document);
582
        }
583 12
    }
584
585
    /**
586
     * Gets the changeset for a document.
587
     *
588
     * @param object $document
589
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
590
     */
591 573
    public function getDocumentChangeSet($document)
592
    {
593 573
        $oid = spl_object_hash($document);
594 573
        if (isset($this->documentChangeSets[$oid])) {
595 569
            return $this->documentChangeSets[$oid];
596
        }
597 55
        return array();
598
    }
599
600
    /**
601
     * INTERNAL:
602
     * Sets the changeset for a document.
603
     *
604
     * @param object $document
605
     * @param array $changeset
606
     */
607 1
    public function setDocumentChangeSet($document, $changeset)
608
    {
609 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
610 1
    }
611
612
    /**
613
     * Get a documents actual data, flattening all the objects to arrays.
614
     *
615
     * @param object $document
616
     * @return array
617
     */
618 580
    public function getDocumentActualData($document)
619
    {
620 580
        $class = $this->dm->getClassMetadata(get_class($document));
621 580
        $actualData = array();
622 580
        foreach ($class->reflFields as $name => $refProp) {
623 580
            $mapping = $class->fieldMappings[$name];
624
            // skip not saved fields
625 580
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
626 27
                continue;
627
            }
628 580
            $value = $refProp->getValue($document);
629 580
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
630 580
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
631
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
632 371
                if ( ! $value instanceof Collection) {
633 139
                    $value = new ArrayCollection($value);
634
                }
635
636
                // Inject PersistentCollection
637 371
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
638 371
                $coll->setOwner($document, $mapping);
639 371
                $coll->setDirty( ! $value->isEmpty());
640 371
                $class->reflFields[$name]->setValue($document, $coll);
641 371
                $actualData[$name] = $coll;
642
            } else {
643 580
                $actualData[$name] = $value;
644
            }
645
        }
646 580
        return $actualData;
647
    }
648
649
    /**
650
     * Computes the changes that happened to a single document.
651
     *
652
     * Modifies/populates the following properties:
653
     *
654
     * {@link originalDocumentData}
655
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
656
     * then it was not fetched from the database and therefore we have no original
657
     * document data yet. All of the current document data is stored as the original document data.
658
     *
659
     * {@link documentChangeSets}
660
     * The changes detected on all properties of the document are stored there.
661
     * A change is a tuple array where the first entry is the old value and the second
662
     * entry is the new value of the property. Changesets are used by persisters
663
     * to INSERT/UPDATE the persistent document state.
664
     *
665
     * {@link documentUpdates}
666
     * If the document is already fully MANAGED (has been fetched from the database before)
667
     * and any changes to its properties are detected, then a reference to the document is stored
668
     * there to mark it for an update.
669
     *
670
     * @param ClassMetadata $class The class descriptor of the document.
671
     * @param object $document The document for which to compute the changes.
672
     */
673 576
    public function computeChangeSet(ClassMetadata $class, $document)
674
    {
675 576
        if ( ! $class->isInheritanceTypeNone()) {
676 175
            $class = $this->dm->getClassMetadata(get_class($document));
677
        }
678
679
        // Fire PreFlush lifecycle callbacks
680 576 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...
681 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
682
        }
683
684 576
        $this->computeOrRecomputeChangeSet($class, $document);
685 575
    }
686
687
    /**
688
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
689
     *
690
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
691
     * @param object $document
692
     * @param boolean $recompute
693
     */
694 576
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
695
    {
696 576
        $oid = spl_object_hash($document);
697 576
        $actualData = $this->getDocumentActualData($document);
698 576
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
699 576
        if ($isNewDocument) {
700
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
701
            // These result in an INSERT.
702 576
            $this->originalDocumentData[$oid] = $actualData;
703 576
            $changeSet = array();
704 576
            foreach ($actualData as $propName => $actualValue) {
705
                /* At this PersistentCollection shouldn't be here, probably it
706
                 * was cloned and its ownership must be fixed
707
                 */
708 576
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
709
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
710
                    $actualValue = $actualData[$propName];
711
                }
712
                // ignore inverse side of reference relationship
713 576 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...
714 184
                    continue;
715
                }
716 576
                $changeSet[$propName] = array(null, $actualValue);
717
            }
718 576
            $this->documentChangeSets[$oid] = $changeSet;
719
        } else {
720 264
            if ($class->isReadOnly) {
721 2
                return;
722
            }
723
            // Document is "fully" MANAGED: it was already fully persisted before
724
            // and we have a copy of the original data
725 262
            $originalData = $this->originalDocumentData[$oid];
726 262
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
727 262
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
728 2
                $changeSet = $this->documentChangeSets[$oid];
729
            } else {
730 262
                $changeSet = array();
731
            }
732
733 262
            foreach ($actualData as $propName => $actualValue) {
734
                // skip not saved fields
735 262
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
736
                    continue;
737
                }
738
739 262
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
740
741
                // skip if value has not changed
742 262
                if ($orgValue === $actualValue) {
743 261
                    if (!$actualValue instanceof PersistentCollectionInterface) {
744 261
                        continue;
745
                    }
746
747 181
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
748
                        // consider dirty collections as changed as well
749 157
                        continue;
750
                    }
751
                }
752
753
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
754 224
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
755 13
                    if ($orgValue !== null) {
756 8
                        $this->scheduleOrphanRemoval($orgValue);
757
                    }
758 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
759 13
                    continue;
760
                }
761
762
                // if owning side of reference-one relationship
763 218
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
764 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
765 1
                        $this->scheduleOrphanRemoval($orgValue);
766
                    }
767
768 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
769 13
                    continue;
770
                }
771
772 211
                if ($isChangeTrackingNotify) {
773 3
                    continue;
774
                }
775
776
                // ignore inverse side of reference relationship
777 209 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...
778 6
                    continue;
779
                }
780
781
                // Persistent collection was exchanged with the "originally"
782
                // created one. This can only mean it was cloned and replaced
783
                // on another document.
784 207
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
785 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
786
                }
787
788
                // if embed-many or reference-many relationship
789 207
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
790 100
                    $changeSet[$propName] = array($orgValue, $actualValue);
791
                    /* If original collection was exchanged with a non-empty value
792
                     * and $set will be issued, there is no need to $unset it first
793
                     */
794 100
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
795 19
                        continue;
796
                    }
797 87
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
798 15
                        $this->scheduleCollectionDeletion($orgValue);
799
                    }
800 87
                    continue;
801
                }
802
803
                // skip equivalent date values
804 136
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
805 37
                    $dateType = Type::getType('date');
806 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
807 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
808
809 37
                    if ($dbOrgValue instanceof \MongoDB\BSON\UTCDateTime && $dbActualValue instanceof \MongoDB\BSON\UTCDateTime && $dbOrgValue == $dbActualValue) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
810 30
                        continue;
811
                    }
812
                }
813
814
                // regular field
815 119
                $changeSet[$propName] = array($orgValue, $actualValue);
816
            }
817 262
            if ($changeSet) {
818 213
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
819 16
                    ? $changeSet + $this->documentChangeSets[$oid]
820 211
                    : $changeSet;
821
822 213
                $this->originalDocumentData[$oid] = $actualData;
823 213
                $this->scheduleForUpdate($document);
824
            }
825
        }
826
827
        // Look for changes in associations of the document
828 576
        $associationMappings = array_filter(
829 576
            $class->associationMappings,
830
            function ($assoc) { return empty($assoc['notSaved']); }
831
        );
832
833 576
        foreach ($associationMappings as $mapping) {
834 437
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
835
836 437
            if ($value === null) {
837 300
                continue;
838
            }
839
840 424
            $this->computeAssociationChanges($document, $mapping, $value);
841
842 423
            if (isset($mapping['reference'])) {
843 320
                continue;
844
            }
845
846 322
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
847
848 322
            foreach ($values as $obj) {
849 158
                $oid2 = spl_object_hash($obj);
850
851 158
                if (isset($this->documentChangeSets[$oid2])) {
852 156
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
853
                        // instance of $value is the same as it was previously otherwise there would be
854
                        // change set already in place
855 34
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
856
                    }
857
858 156
                    if ( ! $isNewDocument) {
859 65
                        $this->scheduleForUpdate($document);
860
                    }
861
862 322
                    break;
863
                }
864
            }
865
        }
866 575
    }
867
868
    /**
869
     * Computes all the changes that have been done to documents and collections
870
     * since the last commit and stores these changes in the _documentChangeSet map
871
     * temporarily for access by the persisters, until the UoW commit is finished.
872
     */
873 575
    public function computeChangeSets()
874
    {
875 575
        $this->computeScheduleInsertsChangeSets();
876 574
        $this->computeScheduleUpsertsChangeSets();
877
878
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
879 574
        foreach ($this->identityMap as $className => $documents) {
880 574
            $class = $this->dm->getClassMetadata($className);
881 574
            if ($class->isEmbeddedDocument) {
882
                /* we do not want to compute changes to embedded documents up front
883
                 * in case embedded document was replaced and its changeset
884
                 * would corrupt data. Embedded documents' change set will
885
                 * be calculated by reachability from owning document.
886
                 */
887 149
                continue;
888
            }
889
890
            // If change tracking is explicit or happens through notification, then only compute
891
            // changes on document of that type that are explicitly marked for synchronization.
892
            switch (true) {
893 574
                case ($class->isChangeTrackingDeferredImplicit()):
894 573
                    $documentsToProcess = $documents;
895 573
                    break;
896
897 4
                case (isset($this->scheduledForDirtyCheck[$className])):
898 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
899 3
                    break;
900
901
                default:
902 4
                    $documentsToProcess = array();
903
904
            }
905
906 574
            foreach ($documentsToProcess as $document) {
907
                // Ignore uninitialized proxy objects
908 570
                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...
909 9
                    continue;
910
                }
911
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
912 570
                $oid = spl_object_hash($document);
913 570 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...
914 570
                    && ! isset($this->documentUpserts[$oid])
915 570
                    && ! isset($this->documentDeletions[$oid])
916 570
                    && isset($this->documentStates[$oid])
917
                ) {
918 574
                    $this->computeChangeSet($class, $document);
919
                }
920
            }
921
        }
922 574
    }
923
924
    /**
925
     * Computes the changes of an association.
926
     *
927
     * @param object $parentDocument
928
     * @param array $assoc
929
     * @param mixed $value The value of the association.
930
     * @throws \InvalidArgumentException
931
     */
932 424
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
933
    {
934 424
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
935 424
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
936 424
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
937
938 424
        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...
939 7
            return;
940
        }
941
942 423
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
943 227
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
944 223
                $this->scheduleCollectionUpdate($value);
945
            }
946 227
            $topmostOwner = $this->getOwningDocument($value->getOwner());
947 227
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
948 227
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
949 122
                $value->initialize();
950 122
                foreach ($value->getDeletedDocuments() as $orphan) {
951 20
                    $this->scheduleOrphanRemoval($orphan);
952
                }
953
            }
954
        }
955
956
        // Look through the documents, and in any of their associations,
957
        // for transient (new) documents, recursively. ("Persistence by reachability")
958
        // Unwrap. Uninitialized collections will simply be empty.
959 423
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
960
961 423
        $count = 0;
962 423
        foreach ($unwrappedValue as $key => $entry) {
963 336
            if ( ! is_object($entry)) {
964 1
                throw new \InvalidArgumentException(
965 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
966
                );
967
            }
968
969 335
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
970
971 335
            $state = $this->getDocumentState($entry, self::STATE_NEW);
972
973
            // Handle "set" strategy for multi-level hierarchy
974 335
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
975 335
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
976
977 335
            $count++;
978
979
            switch ($state) {
980 335
                case self::STATE_NEW:
981 53
                    if ( ! $assoc['isCascadePersist']) {
982
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
983
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
984
                            . ' Explicitly persist the new document or configure cascading persist operations'
985
                            . ' on the relationship.');
986
                    }
987
988 53
                    $this->persistNew($targetClass, $entry);
989 53
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
990 53
                    $this->computeChangeSet($targetClass, $entry);
991 53
                    break;
992
993 331
                case self::STATE_MANAGED:
994 331
                    if ($targetClass->isEmbeddedDocument) {
995 150
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
996 150
                        if ($knownParent && $knownParent !== $parentDocument) {
997 6
                            $entry = clone $entry;
998 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
999 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
1000 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
1001 3
                                $poid = spl_object_hash($parentDocument);
1002 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
1003 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
1004
                                }
1005
                            } else {
1006
                                // must use unwrapped value to not trigger orphan removal
1007 4
                                $unwrappedValue[$key] = $entry;
1008
                            }
1009 6
                            $this->persistNew($targetClass, $entry);
1010
                        }
1011 150
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
1012 150
                        $this->computeChangeSet($targetClass, $entry);
1013
                    }
1014 331
                    break;
1015
1016 1
                case self::STATE_REMOVED:
1017
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1018
                    // and remove the element from Collection.
1019 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1020
                        unset($value[$key]);
1021
                    }
1022 1
                    break;
1023
1024
                case self::STATE_DETACHED:
1025
                    // Can actually not happen right now as we assume STATE_NEW,
1026
                    // so the exception will be raised from the DBAL layer (constraint violation).
1027
                    throw new \InvalidArgumentException('A detached document was found through a '
1028
                        . 'relationship during cascading a persist operation.');
1029
1030 335
                default:
1031
                    // MANAGED associated documents are already taken into account
1032
                    // during changeset calculation anyway, since they are in the identity map.
1033
1034
            }
1035
        }
1036 422
    }
1037
1038
    /**
1039
     * INTERNAL:
1040
     * Computes the changeset of an individual document, independently of the
1041
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1042
     *
1043
     * The passed document must be a managed document. If the document already has a change set
1044
     * because this method is invoked during a commit cycle then the change sets are added.
1045
     * whereby changes detected in this method prevail.
1046
     *
1047
     * @ignore
1048
     * @param ClassMetadata $class The class descriptor of the document.
1049
     * @param object $document The document for which to (re)calculate the change set.
1050
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1051
     */
1052 17
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1053
    {
1054
        // Ignore uninitialized proxy objects
1055 17
        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...
1056 1
            return;
1057
        }
1058
1059 16
        $oid = spl_object_hash($document);
1060
1061 16
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1062
            throw new \InvalidArgumentException('Document must be managed.');
1063
        }
1064
1065 16
        if ( ! $class->isInheritanceTypeNone()) {
1066 1
            $class = $this->dm->getClassMetadata(get_class($document));
1067
        }
1068
1069 16
        $this->computeOrRecomputeChangeSet($class, $document, true);
1070 16
    }
1071
1072
    /**
1073
     * @param ClassMetadata $class
1074
     * @param object $document
1075
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1076
     */
1077 606
    private function persistNew(ClassMetadata $class, $document)
1078
    {
1079 606
        $this->lifecycleEventManager->prePersist($class, $document);
1080 606
        $oid = spl_object_hash($document);
1081 606
        $upsert = false;
1082 606
        if ($class->identifier) {
1083 606
            $idValue = $class->getIdentifierValue($document);
1084 606
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1085
1086 606
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1087 3
                throw new \InvalidArgumentException(sprintf(
1088 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1089 3
                    get_class($document)
1090
                ));
1091
            }
1092
1093 605
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', $idValue)) {
1094 1
                throw new \InvalidArgumentException(sprintf(
1095 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1096 1
                    get_class($document)
1097
                ));
1098
            }
1099
1100 604
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1101 525
                $idValue = $class->idGenerator->generate($this->dm, $document);
1102 525
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1103 525
                $class->setIdentifierValue($document, $idValue);
1104
            }
1105
1106 604
            $this->documentIdentifiers[$oid] = $idValue;
1107
        } else {
1108
            // this is for embedded documents without identifiers
1109 130
            $this->documentIdentifiers[$oid] = $oid;
1110
        }
1111
1112 604
        $this->documentStates[$oid] = self::STATE_MANAGED;
1113
1114 604
        if ($upsert) {
1115 89
            $this->scheduleForUpsert($class, $document);
1116
        } else {
1117 533
            $this->scheduleForInsert($class, $document);
1118
        }
1119 604
    }
1120
1121
    /**
1122
     * Executes all document insertions for documents of the specified type.
1123
     *
1124
     * @param ClassMetadata $class
1125
     * @param array $documents Array of documents to insert
1126
     * @param array $options Array of options to be used with batchInsert()
1127
     */
1128 496 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...
1129
    {
1130 496
        $persister = $this->getDocumentPersister($class->name);
1131
1132 496
        foreach ($documents as $oid => $document) {
1133 496
            $persister->addInsert($document);
1134 496
            unset($this->documentInsertions[$oid]);
1135
        }
1136
1137 496
        $persister->executeInserts($options);
1138
1139 495
        foreach ($documents as $document) {
1140 495
            $this->lifecycleEventManager->postPersist($class, $document);
1141
        }
1142 495
    }
1143
1144
    /**
1145
     * Executes all document upserts for documents of the specified type.
1146
     *
1147
     * @param ClassMetadata $class
1148
     * @param array $documents Array of documents to upsert
1149
     * @param array $options Array of options to be used with batchInsert()
1150
     */
1151 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...
1152
    {
1153 86
        $persister = $this->getDocumentPersister($class->name);
1154
1155
1156 86
        foreach ($documents as $oid => $document) {
1157 86
            $persister->addUpsert($document);
1158 86
            unset($this->documentUpserts[$oid]);
1159
        }
1160
1161 86
        $persister->executeUpserts($options);
1162
1163 86
        foreach ($documents as $document) {
1164 86
            $this->lifecycleEventManager->postPersist($class, $document);
1165
        }
1166 86
    }
1167
1168
    /**
1169
     * Executes all document updates for documents of the specified type.
1170
     *
1171
     * @param Mapping\ClassMetadata $class
1172
     * @param array $documents Array of documents to update
1173
     * @param array $options Array of options to be used with update()
1174
     */
1175 208
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1176
    {
1177 208
        if ($class->isReadOnly) {
1178
            return;
1179
        }
1180
1181 208
        $className = $class->name;
1182 208
        $persister = $this->getDocumentPersister($className);
1183
1184 208
        foreach ($documents as $oid => $document) {
1185 208
            $this->lifecycleEventManager->preUpdate($class, $document);
1186
1187 208
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1188 207
                $persister->update($document, $options);
1189
            }
1190
1191 204
            unset($this->documentUpdates[$oid]);
1192
1193 204
            $this->lifecycleEventManager->postUpdate($class, $document);
1194
        }
1195 204
    }
1196
1197
    /**
1198
     * Executes all document deletions for documents of the specified type.
1199
     *
1200
     * @param ClassMetadata $class
1201
     * @param array $documents Array of documents to delete
1202
     * @param array $options Array of options to be used with remove()
1203
     */
1204 65
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1205
    {
1206 65
        $persister = $this->getDocumentPersister($class->name);
1207
1208 65
        foreach ($documents as $oid => $document) {
1209 65
            if ( ! $class->isEmbeddedDocument) {
1210 33
                $persister->delete($document, $options);
1211
            }
1212
            unset(
1213 63
                $this->documentDeletions[$oid],
1214 63
                $this->documentIdentifiers[$oid],
1215 63
                $this->originalDocumentData[$oid]
1216
            );
1217
1218
            // Clear snapshot information for any referenced PersistentCollection
1219
            // http://www.doctrine-project.org/jira/browse/MODM-95
1220 63
            foreach ($class->associationMappings as $fieldMapping) {
1221 38
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1222 24
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1223 24
                    if ($value instanceof PersistentCollectionInterface) {
1224 38
                        $value->clearSnapshot();
1225
                    }
1226
                }
1227
            }
1228
1229
            // Document with this $oid after deletion treated as NEW, even if the $oid
1230
            // is obtained by a new document because the old one went out of scope.
1231 63
            $this->documentStates[$oid] = self::STATE_NEW;
1232
1233 63
            $this->lifecycleEventManager->postRemove($class, $document);
1234
        }
1235 63
    }
1236
1237
    /**
1238
     * Schedules a document for insertion into the database.
1239
     * If the document already has an identifier, it will be added to the
1240
     * identity map.
1241
     *
1242
     * @param ClassMetadata $class
1243
     * @param object $document The document to schedule for insertion.
1244
     * @throws \InvalidArgumentException
1245
     */
1246 536
    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...
1247
    {
1248 536
        $oid = spl_object_hash($document);
1249
1250 536
        if (isset($this->documentUpdates[$oid])) {
1251
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1252
        }
1253 536
        if (isset($this->documentDeletions[$oid])) {
1254
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1255
        }
1256 536
        if (isset($this->documentInsertions[$oid])) {
1257
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1258
        }
1259
1260 536
        $this->documentInsertions[$oid] = $document;
1261
1262 536
        if (isset($this->documentIdentifiers[$oid])) {
1263 533
            $this->addToIdentityMap($document);
1264
        }
1265 536
    }
1266
1267
    /**
1268
     * Schedules a document for upsert into the database and adds it to the
1269
     * identity map
1270
     *
1271
     * @param ClassMetadata $class
1272
     * @param object $document The document to schedule for upsert.
1273
     * @throws \InvalidArgumentException
1274
     */
1275 92
    public function scheduleForUpsert(ClassMetadata $class, $document)
1276
    {
1277 92
        $oid = spl_object_hash($document);
1278
1279 92
        if ($class->isEmbeddedDocument) {
1280
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1281
        }
1282 92
        if (isset($this->documentUpdates[$oid])) {
1283
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1284
        }
1285 92
        if (isset($this->documentDeletions[$oid])) {
1286
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1287
        }
1288 92
        if (isset($this->documentUpserts[$oid])) {
1289
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1290
        }
1291
1292 92
        $this->documentUpserts[$oid] = $document;
1293 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1294 92
        $this->addToIdentityMap($document);
1295 92
    }
1296
1297
    /**
1298
     * Checks whether a document is scheduled for insertion.
1299
     *
1300
     * @param object $document
1301
     * @return boolean
1302
     */
1303 92
    public function isScheduledForInsert($document)
1304
    {
1305 92
        return isset($this->documentInsertions[spl_object_hash($document)]);
1306
    }
1307
1308
    /**
1309
     * Checks whether a document is scheduled for upsert.
1310
     *
1311
     * @param object $document
1312
     * @return boolean
1313
     */
1314 5
    public function isScheduledForUpsert($document)
1315
    {
1316 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1317
    }
1318
1319
    /**
1320
     * Schedules a document for being updated.
1321
     *
1322
     * @param object $document The document to schedule for being updated.
1323
     * @throws \InvalidArgumentException
1324
     */
1325 214
    public function scheduleForUpdate($document)
1326
    {
1327 214
        $oid = spl_object_hash($document);
1328 214
        if ( ! isset($this->documentIdentifiers[$oid])) {
1329
            throw new \InvalidArgumentException('Document has no identity.');
1330
        }
1331
1332 214
        if (isset($this->documentDeletions[$oid])) {
1333
            throw new \InvalidArgumentException('Document is removed.');
1334
        }
1335
1336 214
        if ( ! isset($this->documentUpdates[$oid])
1337 214
            && ! isset($this->documentInsertions[$oid])
1338 214
            && ! isset($this->documentUpserts[$oid])) {
1339 213
            $this->documentUpdates[$oid] = $document;
1340
        }
1341 214
    }
1342
1343
    /**
1344
     * Checks whether a document is registered as dirty in the unit of work.
1345
     * Note: Is not very useful currently as dirty documents are only registered
1346
     * at commit time.
1347
     *
1348
     * @param object $document
1349
     * @return boolean
1350
     */
1351 15
    public function isScheduledForUpdate($document)
1352
    {
1353 15
        return isset($this->documentUpdates[spl_object_hash($document)]);
1354
    }
1355
1356 1
    public function isScheduledForDirtyCheck($document)
1357
    {
1358 1
        $class = $this->dm->getClassMetadata(get_class($document));
1359 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1360
    }
1361
1362
    /**
1363
     * INTERNAL:
1364
     * Schedules a document for deletion.
1365
     *
1366
     * @param object $document
1367
     */
1368 70
    public function scheduleForDelete($document)
1369
    {
1370 70
        $oid = spl_object_hash($document);
1371
1372 70
        if (isset($this->documentInsertions[$oid])) {
1373 1
            if ($this->isInIdentityMap($document)) {
1374 1
                $this->removeFromIdentityMap($document);
1375
            }
1376 1
            unset($this->documentInsertions[$oid]);
1377 1
            return; // document has not been persisted yet, so nothing more to do.
1378
        }
1379
1380 69
        if ( ! $this->isInIdentityMap($document)) {
1381 2
            return; // ignore
1382
        }
1383
1384 68
        $this->removeFromIdentityMap($document);
1385 68
        $this->documentStates[$oid] = self::STATE_REMOVED;
1386
1387 68
        if (isset($this->documentUpdates[$oid])) {
1388
            unset($this->documentUpdates[$oid]);
1389
        }
1390 68
        if ( ! isset($this->documentDeletions[$oid])) {
1391 68
            $this->documentDeletions[$oid] = $document;
1392
        }
1393 68
    }
1394
1395
    /**
1396
     * Checks whether a document is registered as removed/deleted with the unit
1397
     * of work.
1398
     *
1399
     * @param object $document
1400
     * @return boolean
1401
     */
1402 8
    public function isScheduledForDelete($document)
1403
    {
1404 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1405
    }
1406
1407
    /**
1408
     * Checks whether a document is scheduled for insertion, update or deletion.
1409
     *
1410
     * @param $document
1411
     * @return boolean
1412
     */
1413 226
    public function isDocumentScheduled($document)
1414
    {
1415 226
        $oid = spl_object_hash($document);
1416 226
        return isset($this->documentInsertions[$oid]) ||
1417 112
            isset($this->documentUpserts[$oid]) ||
1418 103
            isset($this->documentUpdates[$oid]) ||
1419 226
            isset($this->documentDeletions[$oid]);
1420
    }
1421
1422
    /**
1423
     * INTERNAL:
1424
     * Registers a document in the identity map.
1425
     *
1426
     * Note that documents in a hierarchy are registered with the class name of
1427
     * the root document. Identifiers are serialized before being used as array
1428
     * keys to allow differentiation of equal, but not identical, values.
1429
     *
1430
     * @ignore
1431
     * @param object $document  The document to register.
1432
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1433
     *                  the document in question is already managed.
1434
     */
1435 636
    public function addToIdentityMap($document)
1436
    {
1437 636
        $class = $this->dm->getClassMetadata(get_class($document));
1438 636
        $id = $this->getIdForIdentityMap($document);
1439
1440 636
        if (isset($this->identityMap[$class->name][$id])) {
1441 57
            return false;
1442
        }
1443
1444 636
        $this->identityMap[$class->name][$id] = $document;
1445
1446 636
        if ($document instanceof NotifyPropertyChanged &&
1447 636
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1448 3
            $document->addPropertyChangedListener($this);
1449
        }
1450
1451 636
        return true;
1452
    }
1453
1454
    /**
1455
     * Gets the state of a document with regard to the current unit of work.
1456
     *
1457
     * @param object   $document
1458
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1459
     *                         This parameter can be set to improve performance of document state detection
1460
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1461
     *                         is either known or does not matter for the caller of the method.
1462
     * @return int The document state.
1463
     */
1464 609
    public function getDocumentState($document, $assume = null)
1465
    {
1466 609
        $oid = spl_object_hash($document);
1467
1468 609
        if (isset($this->documentStates[$oid])) {
1469 376
            return $this->documentStates[$oid];
1470
        }
1471
1472 609
        $class = $this->dm->getClassMetadata(get_class($document));
1473
1474 609
        if ($class->isEmbeddedDocument) {
1475 163
            return self::STATE_NEW;
1476
        }
1477
1478 606
        if ($assume !== null) {
1479 603
            return $assume;
1480
        }
1481
1482
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1483
         * known. Note that you cannot remember the NEW or DETACHED state in
1484
         * _documentStates since the UoW does not hold references to such
1485
         * objects and the object hash can be reused. More generally, because
1486
         * the state may "change" between NEW/DETACHED without the UoW being
1487
         * aware of it.
1488
         */
1489 4
        $id = $class->getIdentifierObject($document);
1490
1491 4
        if ($id === null) {
1492 3
            return self::STATE_NEW;
1493
        }
1494
1495
        // Check for a version field, if available, to avoid a DB lookup.
1496 2
        if ($class->isVersioned) {
1497
            return $class->getFieldValue($document, $class->versionField)
1498
                ? self::STATE_DETACHED
1499
                : self::STATE_NEW;
1500
        }
1501
1502
        // Last try before DB lookup: check the identity map.
1503 2
        if ($this->tryGetById($id, $class)) {
1504 1
            return self::STATE_DETACHED;
1505
        }
1506
1507
        // DB lookup
1508 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1509 1
            return self::STATE_DETACHED;
1510
        }
1511
1512 1
        return self::STATE_NEW;
1513
    }
1514
1515
    /**
1516
     * INTERNAL:
1517
     * Removes a document from the identity map. This effectively detaches the
1518
     * document from the persistence management of Doctrine.
1519
     *
1520
     * @ignore
1521
     * @param object $document
1522
     * @throws \InvalidArgumentException
1523
     * @return boolean
1524
     */
1525 82
    public function removeFromIdentityMap($document)
1526
    {
1527 82
        $oid = spl_object_hash($document);
1528
1529
        // Check if id is registered first
1530 82
        if ( ! isset($this->documentIdentifiers[$oid])) {
1531
            return false;
1532
        }
1533
1534 82
        $class = $this->dm->getClassMetadata(get_class($document));
1535 82
        $id = $this->getIdForIdentityMap($document);
1536
1537 82
        if (isset($this->identityMap[$class->name][$id])) {
1538 82
            unset($this->identityMap[$class->name][$id]);
1539 82
            $this->documentStates[$oid] = self::STATE_DETACHED;
1540 82
            return true;
1541
        }
1542
1543
        return false;
1544
    }
1545
1546
    /**
1547
     * INTERNAL:
1548
     * Gets a document in the identity map by its identifier hash.
1549
     *
1550
     * @ignore
1551
     * @param mixed         $id    Document identifier
1552
     * @param ClassMetadata $class Document class
1553
     * @return object
1554
     * @throws InvalidArgumentException if the class does not have an identifier
1555
     */
1556 38
    public function getById($id, ClassMetadata $class)
1557
    {
1558 38
        if ( ! $class->identifier) {
1559
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1560
        }
1561
1562 38
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1563
1564 38
        return $this->identityMap[$class->name][$serializedId];
1565
    }
1566
1567
    /**
1568
     * INTERNAL:
1569
     * Tries to get a document by its identifier hash. If no document is found
1570
     * for the given hash, FALSE is returned.
1571
     *
1572
     * @ignore
1573
     * @param mixed         $id    Document identifier
1574
     * @param ClassMetadata $class Document class
1575
     * @return mixed The found document or FALSE.
1576
     * @throws InvalidArgumentException if the class does not have an identifier
1577
     */
1578 298
    public function tryGetById($id, ClassMetadata $class)
1579
    {
1580 298
        if ( ! $class->identifier) {
1581
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1582
        }
1583
1584 298
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1585
1586 298
        return isset($this->identityMap[$class->name][$serializedId]) ?
1587 298
            $this->identityMap[$class->name][$serializedId] : false;
1588
    }
1589
1590
    /**
1591
     * Schedules a document for dirty-checking at commit-time.
1592
     *
1593
     * @param object $document The document to schedule for dirty-checking.
1594
     * @todo Rename: scheduleForSynchronization
1595
     */
1596 3
    public function scheduleForDirtyCheck($document)
1597
    {
1598 3
        $class = $this->dm->getClassMetadata(get_class($document));
1599 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1600 3
    }
1601
1602
    /**
1603
     * Checks whether a document is registered in the identity map.
1604
     *
1605
     * @param object $document
1606
     * @return boolean
1607
     */
1608 81
    public function isInIdentityMap($document)
1609
    {
1610 81
        $oid = spl_object_hash($document);
1611
1612 81
        if ( ! isset($this->documentIdentifiers[$oid])) {
1613 6
            return false;
1614
        }
1615
1616 79
        $class = $this->dm->getClassMetadata(get_class($document));
1617 79
        $id = $this->getIdForIdentityMap($document);
1618
1619 79
        return isset($this->identityMap[$class->name][$id]);
1620
    }
1621
1622
    /**
1623
     * @param object $document
1624
     * @return string
1625
     */
1626 636
    private function getIdForIdentityMap($document)
1627
    {
1628 636
        $class = $this->dm->getClassMetadata(get_class($document));
1629
1630 636
        if ( ! $class->identifier) {
1631 133
            $id = spl_object_hash($document);
1632
        } else {
1633 635
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1634 635
            $id = serialize($class->getDatabaseIdentifierValue($id));
1635
        }
1636
1637 636
        return $id;
1638
    }
1639
1640
    /**
1641
     * INTERNAL:
1642
     * Checks whether an identifier exists in the identity map.
1643
     *
1644
     * @ignore
1645
     * @param string $id
1646
     * @param string $rootClassName
1647
     * @return boolean
1648
     */
1649
    public function containsId($id, $rootClassName)
1650
    {
1651
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1652
    }
1653
1654
    /**
1655
     * Persists a document as part of the current unit of work.
1656
     *
1657
     * @param object $document The document to persist.
1658
     * @throws MongoDBException If trying to persist MappedSuperclass.
1659
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1660
     */
1661 606
    public function persist($document)
1662
    {
1663 606
        $class = $this->dm->getClassMetadata(get_class($document));
1664 606
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1665 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1666
        }
1667 605
        $visited = array();
1668 605
        $this->doPersist($document, $visited);
1669 601
    }
1670
1671
    /**
1672
     * Saves a document as part of the current unit of work.
1673
     * This method is internally called during save() cascades as it tracks
1674
     * the already visited documents to prevent infinite recursions.
1675
     *
1676
     * NOTE: This method always considers documents that are not yet known to
1677
     * this UnitOfWork as NEW.
1678
     *
1679
     * @param object $document The document to persist.
1680
     * @param array $visited The already visited documents.
1681
     * @throws \InvalidArgumentException
1682
     * @throws MongoDBException
1683
     */
1684 605
    private function doPersist($document, array &$visited)
1685
    {
1686 605
        $oid = spl_object_hash($document);
1687 605
        if (isset($visited[$oid])) {
1688 25
            return; // Prevent infinite recursion
1689
        }
1690
1691 605
        $visited[$oid] = $document; // Mark visited
1692
1693 605
        $class = $this->dm->getClassMetadata(get_class($document));
1694
1695 605
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1696
        switch ($documentState) {
1697 605
            case self::STATE_MANAGED:
1698
                // Nothing to do, except if policy is "deferred explicit"
1699 58
                if ($class->isChangeTrackingDeferredExplicit()) {
1700
                    $this->scheduleForDirtyCheck($document);
1701
                }
1702 58
                break;
1703 605
            case self::STATE_NEW:
1704 605
                $this->persistNew($class, $document);
1705 603
                break;
1706
1707 2
            case self::STATE_REMOVED:
1708
                // Document becomes managed again
1709 2
                unset($this->documentDeletions[$oid]);
1710
1711 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1712 2
                break;
1713
1714
            case self::STATE_DETACHED:
1715
                throw new \InvalidArgumentException(
1716
                    'Behavior of persist() for a detached document is not yet defined.');
1717
1718
            default:
1719
                throw MongoDBException::invalidDocumentState($documentState);
1720
        }
1721
1722 603
        $this->cascadePersist($document, $visited);
1723 601
    }
1724
1725
    /**
1726
     * Deletes a document as part of the current unit of work.
1727
     *
1728
     * @param object $document The document to remove.
1729
     */
1730 69
    public function remove($document)
1731
    {
1732 69
        $visited = array();
1733 69
        $this->doRemove($document, $visited);
1734 69
    }
1735
1736
    /**
1737
     * Deletes a document as part of the current unit of work.
1738
     *
1739
     * This method is internally called during delete() cascades as it tracks
1740
     * the already visited documents to prevent infinite recursions.
1741
     *
1742
     * @param object $document The document to delete.
1743
     * @param array $visited The map of the already visited documents.
1744
     * @throws MongoDBException
1745
     */
1746 69
    private function doRemove($document, array &$visited)
1747
    {
1748 69
        $oid = spl_object_hash($document);
1749 69
        if (isset($visited[$oid])) {
1750 1
            return; // Prevent infinite recursion
1751
        }
1752
1753 69
        $visited[$oid] = $document; // mark visited
1754
1755
        /* Cascade first, because scheduleForDelete() removes the entity from
1756
         * the identity map, which can cause problems when a lazy Proxy has to
1757
         * be initialized for the cascade operation.
1758
         */
1759 69
        $this->cascadeRemove($document, $visited);
1760
1761 69
        $class = $this->dm->getClassMetadata(get_class($document));
1762 69
        $documentState = $this->getDocumentState($document);
1763
        switch ($documentState) {
1764 69
            case self::STATE_NEW:
1765 69
            case self::STATE_REMOVED:
1766
                // nothing to do
1767
                break;
1768 69
            case self::STATE_MANAGED:
1769 69
                $this->lifecycleEventManager->preRemove($class, $document);
1770 69
                $this->scheduleForDelete($document);
1771 69
                break;
1772
            case self::STATE_DETACHED:
1773
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1774
            default:
1775
                throw MongoDBException::invalidDocumentState($documentState);
1776
        }
1777 69
    }
1778
1779
    /**
1780
     * Merges the state of the given detached document into this UnitOfWork.
1781
     *
1782
     * @param object $document
1783
     * @return object The managed copy of the document.
1784
     */
1785 12
    public function merge($document)
1786
    {
1787 12
        $visited = array();
1788
1789 12
        return $this->doMerge($document, $visited);
1790
    }
1791
1792
    /**
1793
     * Executes a merge operation on a document.
1794
     *
1795
     * @param object      $document
1796
     * @param array       $visited
1797
     * @param object|null $prevManagedCopy
1798
     * @param array|null  $assoc
1799
     *
1800
     * @return object The managed copy of the document.
1801
     *
1802
     * @throws InvalidArgumentException If the entity instance is NEW.
1803
     * @throws LockException If the document uses optimistic locking through a
1804
     *                       version attribute and the version check against the
1805
     *                       managed copy fails.
1806
     */
1807 12
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1808
    {
1809 12
        $oid = spl_object_hash($document);
1810
1811 12
        if (isset($visited[$oid])) {
1812 1
            return $visited[$oid]; // Prevent infinite recursion
1813
        }
1814
1815 12
        $visited[$oid] = $document; // mark visited
1816
1817 12
        $class = $this->dm->getClassMetadata(get_class($document));
1818
1819
        /* First we assume DETACHED, although it can still be NEW but we can
1820
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1821
         * an identity, we need to fetch it from the DB anyway in order to
1822
         * merge. MANAGED documents are ignored by the merge operation.
1823
         */
1824 12
        $managedCopy = $document;
1825
1826 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1827 12
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1828
                $document->__load();
1829
            }
1830
1831 12
            $identifier = $class->getIdentifier();
1832
            // We always have one element in the identifier array but it might be null
1833 12
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1834 12
            $managedCopy = null;
1835
1836
            // Try to fetch document from the database
1837 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1838 12
                $managedCopy = $this->dm->find($class->name, $id);
1839
1840
                // Managed copy may be removed in which case we can't merge
1841 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1842
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1843
                }
1844
1845 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...
1846
                    $managedCopy->__load();
1847
                }
1848
            }
1849
1850 12
            if ($managedCopy === null) {
1851
                // Create a new managed instance
1852 4
                $managedCopy = $class->newInstance();
1853 4
                if ($id !== null) {
1854 3
                    $class->setIdentifierValue($managedCopy, $id);
1855
                }
1856 4
                $this->persistNew($class, $managedCopy);
1857
            }
1858
1859 12
            if ($class->isVersioned) {
1860
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1861
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1862
1863
                // Throw exception if versions don't match
1864
                if ($managedCopyVersion != $documentVersion) {
1865
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1866
                }
1867
            }
1868
1869
            // Merge state of $document into existing (managed) document
1870 12
            foreach ($class->reflClass->getProperties() as $prop) {
1871 12
                $name = $prop->name;
1872 12
                $prop->setAccessible(true);
1873 12
                if ( ! isset($class->associationMappings[$name])) {
1874 12
                    if ( ! $class->isIdentifier($name)) {
1875 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1876
                    }
1877
                } else {
1878 12
                    $assoc2 = $class->associationMappings[$name];
1879
1880 12
                    if ($assoc2['type'] === 'one') {
1881 6
                        $other = $prop->getValue($document);
1882
1883 6
                        if ($other === null) {
1884 2
                            $prop->setValue($managedCopy, null);
1885 5
                        } 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...
1886
                            // Do not merge fields marked lazy that have not been fetched
1887 1
                            continue;
1888 4
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1889
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1890
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1891
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1892
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1893
                                $relatedId = $targetClass->getIdentifierObject($other);
1894
1895
                                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...
1896
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1897
                                } else {
1898
                                    $other = $this
1899
                                        ->dm
1900
                                        ->getProxyFactory()
1901
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1902
                                    $this->registerManaged($other, $relatedId, array());
1903
                                }
1904
                            }
1905
1906 5
                            $prop->setValue($managedCopy, $other);
1907
                        }
1908
                    } else {
1909 10
                        $mergeCol = $prop->getValue($document);
1910
1911 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1912
                            /* Do not merge fields marked lazy that have not
1913
                             * been fetched. Keep the lazy persistent collection
1914
                             * of the managed copy.
1915
                             */
1916 3
                            continue;
1917
                        }
1918
1919 10
                        $managedCol = $prop->getValue($managedCopy);
1920
1921 10
                        if ( ! $managedCol) {
1922 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1923 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1924 1
                            $prop->setValue($managedCopy, $managedCol);
1925 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1926
                        }
1927
1928
                        /* Note: do not process association's target documents.
1929
                         * They will be handled during the cascade. Initialize
1930
                         * and, if necessary, clear $managedCol for now.
1931
                         */
1932 10
                        if ($assoc2['isCascadeMerge']) {
1933 10
                            $managedCol->initialize();
1934
1935
                            // If $managedCol differs from the merged collection, clear and set dirty
1936 10
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1937 3
                                $managedCol->unwrap()->clear();
1938 3
                                $managedCol->setDirty(true);
1939
1940 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1941
                                    $this->scheduleForDirtyCheck($managedCopy);
1942
                                }
1943
                            }
1944
                        }
1945
                    }
1946
                }
1947
1948 12
                if ($class->isChangeTrackingNotify()) {
1949
                    // Just treat all properties as changed, there is no other choice.
1950 12
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1951
                }
1952
            }
1953
1954 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1955
                $this->scheduleForDirtyCheck($document);
1956
            }
1957
        }
1958
1959 12
        if ($prevManagedCopy !== null) {
1960 5
            $assocField = $assoc['fieldName'];
1961 5
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1962
1963 5
            if ($assoc['type'] === 'one') {
1964 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1965
            } else {
1966 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1967
1968 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1969 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1970
                }
1971
            }
1972
        }
1973
1974
        // Mark the managed copy visited as well
1975 12
        $visited[spl_object_hash($managedCopy)] = true;
1976
1977 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1978
1979 12
        return $managedCopy;
1980
    }
1981
1982
    /**
1983
     * Detaches a document from the persistence management. It's persistence will
1984
     * no longer be managed by Doctrine.
1985
     *
1986
     * @param object $document The document to detach.
1987
     */
1988 11
    public function detach($document)
1989
    {
1990 11
        $visited = array();
1991 11
        $this->doDetach($document, $visited);
1992 11
    }
1993
1994
    /**
1995
     * Executes a detach operation on the given document.
1996
     *
1997
     * @param object $document
1998
     * @param array $visited
1999
     * @internal This method always considers documents with an assigned identifier as DETACHED.
2000
     */
2001 16
    private function doDetach($document, array &$visited)
2002
    {
2003 16
        $oid = spl_object_hash($document);
2004 16
        if (isset($visited[$oid])) {
2005 3
            return; // Prevent infinite recursion
2006
        }
2007
2008 16
        $visited[$oid] = $document; // mark visited
2009
2010 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2011 16
            case self::STATE_MANAGED:
2012 16
                $this->removeFromIdentityMap($document);
2013 16
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2014 16
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2015 16
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2016 16
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2017 16
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2018 16
                break;
2019 3
            case self::STATE_NEW:
2020 3
            case self::STATE_DETACHED:
2021 3
                return;
2022
        }
2023
2024 16
        $this->cascadeDetach($document, $visited);
2025 16
    }
2026
2027
    /**
2028
     * Refreshes the state of the given document from the database, overwriting
2029
     * any local, unpersisted changes.
2030
     *
2031
     * @param object $document The document to refresh.
2032
     * @throws \InvalidArgumentException If the document is not MANAGED.
2033
     */
2034 21
    public function refresh($document)
2035
    {
2036 21
        $visited = array();
2037 21
        $this->doRefresh($document, $visited);
2038 20
    }
2039
2040
    /**
2041
     * Executes a refresh operation on a document.
2042
     *
2043
     * @param object $document The document to refresh.
2044
     * @param array $visited The already visited documents during cascades.
2045
     * @throws \InvalidArgumentException If the document is not MANAGED.
2046
     */
2047 21
    private function doRefresh($document, array &$visited)
2048
    {
2049 21
        $oid = spl_object_hash($document);
2050 21
        if (isset($visited[$oid])) {
2051
            return; // Prevent infinite recursion
2052
        }
2053
2054 21
        $visited[$oid] = $document; // mark visited
2055
2056 21
        $class = $this->dm->getClassMetadata(get_class($document));
2057
2058 21
        if ( ! $class->isEmbeddedDocument) {
2059 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2060 20
                $this->getDocumentPersister($class->name)->refresh($document);
2061
            } else {
2062 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2063
            }
2064
        }
2065
2066 20
        $this->cascadeRefresh($document, $visited);
2067 20
    }
2068
2069
    /**
2070
     * Cascades a refresh operation to associated documents.
2071
     *
2072
     * @param object $document
2073
     * @param array $visited
2074
     */
2075 20
    private function cascadeRefresh($document, array &$visited)
2076
    {
2077 20
        $class = $this->dm->getClassMetadata(get_class($document));
2078
2079 20
        $associationMappings = array_filter(
2080 20
            $class->associationMappings,
2081
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2082
        );
2083
2084 20
        foreach ($associationMappings as $mapping) {
2085 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2086 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2087 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2088
                    // Unwrap so that foreach() does not initialize
2089 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2090
                }
2091 15
                foreach ($relatedDocuments as $relatedDocument) {
2092 15
                    $this->doRefresh($relatedDocument, $visited);
2093
                }
2094 10
            } elseif ($relatedDocuments !== null) {
2095 15
                $this->doRefresh($relatedDocuments, $visited);
2096
            }
2097
        }
2098 20
    }
2099
2100
    /**
2101
     * Cascades a detach operation to associated documents.
2102
     *
2103
     * @param object $document
2104
     * @param array $visited
2105
     */
2106 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...
2107
    {
2108 16
        $class = $this->dm->getClassMetadata(get_class($document));
2109 16
        foreach ($class->fieldMappings as $mapping) {
2110 16
            if ( ! $mapping['isCascadeDetach']) {
2111 16
                continue;
2112
            }
2113 10
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2114 10
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2115 10
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2116
                    // Unwrap so that foreach() does not initialize
2117 7
                    $relatedDocuments = $relatedDocuments->unwrap();
2118
                }
2119 10
                foreach ($relatedDocuments as $relatedDocument) {
2120 10
                    $this->doDetach($relatedDocument, $visited);
2121
                }
2122 10
            } elseif ($relatedDocuments !== null) {
2123 10
                $this->doDetach($relatedDocuments, $visited);
2124
            }
2125
        }
2126 16
    }
2127
    /**
2128
     * Cascades a merge operation to associated documents.
2129
     *
2130
     * @param object $document
2131
     * @param object $managedCopy
2132
     * @param array $visited
2133
     */
2134 12
    private function cascadeMerge($document, $managedCopy, array &$visited)
2135
    {
2136 12
        $class = $this->dm->getClassMetadata(get_class($document));
2137
2138 12
        $associationMappings = array_filter(
2139 12
            $class->associationMappings,
2140
            function ($assoc) { return $assoc['isCascadeMerge']; }
2141
        );
2142
2143 12
        foreach ($associationMappings as $assoc) {
2144 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2145
2146 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2147 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2148
                    // Collections are the same, so there is nothing to do
2149 1
                    continue;
2150
                }
2151
2152 8
                foreach ($relatedDocuments as $relatedDocument) {
2153 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2154
                }
2155 6
            } elseif ($relatedDocuments !== null) {
2156 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2157
            }
2158
        }
2159 12
    }
2160
2161
    /**
2162
     * Cascades the save operation to associated documents.
2163
     *
2164
     * @param object $document
2165
     * @param array $visited
2166
     */
2167 603
    private function cascadePersist($document, array &$visited)
2168
    {
2169 603
        $class = $this->dm->getClassMetadata(get_class($document));
2170
2171 603
        $associationMappings = array_filter(
2172 603
            $class->associationMappings,
2173
            function ($assoc) { return $assoc['isCascadePersist']; }
2174
        );
2175
2176 603
        foreach ($associationMappings as $fieldName => $mapping) {
2177 417
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2178
2179 417
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2180 344
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2181 17
                    if ($relatedDocuments->getOwner() !== $document) {
2182 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2183
                    }
2184
                    // Unwrap so that foreach() does not initialize
2185 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2186
                }
2187
2188 344
                $count = 0;
2189 344
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2190 174
                    if ( ! empty($mapping['embedded'])) {
2191 103
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2192 103
                        if ($knownParent && $knownParent !== $document) {
2193 1
                            $relatedDocument = clone $relatedDocument;
2194 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2195
                        }
2196 103
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2197 103
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2198
                    }
2199 344
                    $this->doPersist($relatedDocument, $visited);
2200
                }
2201 332
            } elseif ($relatedDocuments !== null) {
2202 130
                if ( ! empty($mapping['embedded'])) {
2203 67
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2204 67
                    if ($knownParent && $knownParent !== $document) {
2205 3
                        $relatedDocuments = clone $relatedDocuments;
2206 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2207
                    }
2208 67
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2209
                }
2210 417
                $this->doPersist($relatedDocuments, $visited);
2211
            }
2212
        }
2213 601
    }
2214
2215
    /**
2216
     * Cascades the delete operation to associated documents.
2217
     *
2218
     * @param object $document
2219
     * @param array $visited
2220
     */
2221 69 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...
2222
    {
2223 69
        $class = $this->dm->getClassMetadata(get_class($document));
2224 69
        foreach ($class->fieldMappings as $mapping) {
2225 69
            if ( ! $mapping['isCascadeRemove']) {
2226 68
                continue;
2227
            }
2228 33
            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...
2229 2
                $document->__load();
2230
            }
2231
2232 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2233 33
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2234
                // If its a PersistentCollection initialization is intended! No unwrap!
2235 22
                foreach ($relatedDocuments as $relatedDocument) {
2236 22
                    $this->doRemove($relatedDocument, $visited);
2237
                }
2238 22
            } elseif ($relatedDocuments !== null) {
2239 33
                $this->doRemove($relatedDocuments, $visited);
2240
            }
2241
        }
2242 69
    }
2243
2244
    /**
2245
     * Acquire a lock on the given document.
2246
     *
2247
     * @param object $document
2248
     * @param int $lockMode
2249
     * @param int $lockVersion
2250
     * @throws LockException
2251
     * @throws \InvalidArgumentException
2252
     */
2253 8
    public function lock($document, $lockMode, $lockVersion = null)
2254
    {
2255 8
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2256 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2257
        }
2258
2259 7
        $documentName = get_class($document);
2260 7
        $class = $this->dm->getClassMetadata($documentName);
2261
2262 7
        if ($lockMode == LockMode::OPTIMISTIC) {
2263 2
            if ( ! $class->isVersioned) {
2264 1
                throw LockException::notVersioned($documentName);
2265
            }
2266
2267 1
            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...
2268 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2269 1
                if ($documentVersion != $lockVersion) {
2270 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2271
                }
2272
            }
2273 5
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2274 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2275
        }
2276 5
    }
2277
2278
    /**
2279
     * Releases a lock on the given document.
2280
     *
2281
     * @param object $document
2282
     * @throws \InvalidArgumentException
2283
     */
2284 1
    public function unlock($document)
2285
    {
2286 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2287
            throw new \InvalidArgumentException("Document is not MANAGED.");
2288
        }
2289 1
        $documentName = get_class($document);
2290 1
        $this->getDocumentPersister($documentName)->unlock($document);
2291 1
    }
2292
2293
    /**
2294
     * Clears the UnitOfWork.
2295
     *
2296
     * @param string|null $documentName if given, only documents of this type will get detached.
2297
     */
2298 377
    public function clear($documentName = null)
2299
    {
2300 377
        if ($documentName === null) {
2301 369
            $this->identityMap =
2302 369
            $this->documentIdentifiers =
2303 369
            $this->originalDocumentData =
2304 369
            $this->documentChangeSets =
2305 369
            $this->documentStates =
2306 369
            $this->scheduledForDirtyCheck =
2307 369
            $this->documentInsertions =
2308 369
            $this->documentUpserts =
2309 369
            $this->documentUpdates =
2310 369
            $this->documentDeletions =
2311 369
            $this->collectionUpdates =
2312 369
            $this->collectionDeletions =
2313 369
            $this->parentAssociations =
2314 369
            $this->embeddedDocumentsRegistry =
2315 369
            $this->orphanRemovals =
2316 369
            $this->hasScheduledCollections = array();
2317
        } else {
2318 8
            $visited = array();
2319 8
            foreach ($this->identityMap as $className => $documents) {
2320 8
                if ($className === $documentName) {
2321 5
                    foreach ($documents as $document) {
2322 8
                        $this->doDetach($document, $visited);
2323
                    }
2324
                }
2325
            }
2326
        }
2327
2328 377 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...
2329
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2330
        }
2331 377
    }
2332
2333
    /**
2334
     * INTERNAL:
2335
     * Schedules an embedded document for removal. The remove() operation will be
2336
     * invoked on that document at the beginning of the next commit of this
2337
     * UnitOfWork.
2338
     *
2339
     * @ignore
2340
     * @param object $document
2341
     */
2342 47
    public function scheduleOrphanRemoval($document)
2343
    {
2344 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2345 47
    }
2346
2347
    /**
2348
     * INTERNAL:
2349
     * Unschedules an embedded or referenced object for removal.
2350
     *
2351
     * @ignore
2352
     * @param object $document
2353
     */
2354 100
    public function unscheduleOrphanRemoval($document)
2355
    {
2356 100
        $oid = spl_object_hash($document);
2357 100
        if (isset($this->orphanRemovals[$oid])) {
2358 1
            unset($this->orphanRemovals[$oid]);
2359
        }
2360 100
    }
2361
2362
    /**
2363
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2364
     *  1) sets owner if it was cloned
2365
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2366
     *  3) NOP if state is OK
2367
     * Returned collection should be used from now on (only important with 2nd point)
2368
     *
2369
     * @param PersistentCollectionInterface $coll
2370
     * @param object $document
2371
     * @param ClassMetadata $class
2372
     * @param string $propName
2373
     * @return PersistentCollectionInterface
2374
     */
2375 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2376
    {
2377 8
        $owner = $coll->getOwner();
2378 8
        if ($owner === null) { // cloned
2379 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2380 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2381 2
            if ( ! $coll->isInitialized()) {
2382 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2383
            }
2384 2
            $newValue = clone $coll;
2385 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2386 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2387 2
            if ($this->isScheduledForUpdate($document)) {
2388
                // @todo following line should be superfluous once collections are stored in change sets
2389
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2390
            }
2391 2
            return $newValue;
2392
        }
2393 6
        return $coll;
2394
    }
2395
2396
    /**
2397
     * INTERNAL:
2398
     * Schedules a complete collection for removal when this UnitOfWork commits.
2399
     *
2400
     * @param PersistentCollectionInterface $coll
2401
     */
2402 35
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2403
    {
2404 35
        $oid = spl_object_hash($coll);
2405 35
        unset($this->collectionUpdates[$oid]);
2406 35
        if ( ! isset($this->collectionDeletions[$oid])) {
2407 35
            $this->collectionDeletions[$oid] = $coll;
2408 35
            $this->scheduleCollectionOwner($coll);
2409
        }
2410 35
    }
2411
2412
    /**
2413
     * Checks whether a PersistentCollection is scheduled for deletion.
2414
     *
2415
     * @param PersistentCollectionInterface $coll
2416
     * @return boolean
2417
     */
2418 195
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2419
    {
2420 195
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2421
    }
2422
2423
    /**
2424
     * INTERNAL:
2425
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2426
     *
2427
     * @param PersistentCollectionInterface $coll
2428
     */
2429 202 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...
2430
    {
2431 202
        $oid = spl_object_hash($coll);
2432 202
        if (isset($this->collectionDeletions[$oid])) {
2433 5
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2434 5
            unset($this->collectionDeletions[$oid]);
2435 5
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2436
        }
2437 202
    }
2438
2439
    /**
2440
     * INTERNAL:
2441
     * Schedules a collection for update when this UnitOfWork commits.
2442
     *
2443
     * @param PersistentCollectionInterface $coll
2444
     */
2445 223
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2446
    {
2447 223
        $mapping = $coll->getMapping();
2448 223
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2449
            /* There is no need to $unset collection if it will be $set later
2450
             * This is NOP if collection is not scheduled for deletion
2451
             */
2452 23
            $this->unscheduleCollectionDeletion($coll);
2453
        }
2454 223
        $oid = spl_object_hash($coll);
2455 223
        if ( ! isset($this->collectionUpdates[$oid])) {
2456 223
            $this->collectionUpdates[$oid] = $coll;
2457 223
            $this->scheduleCollectionOwner($coll);
2458
        }
2459 223
    }
2460
2461
    /**
2462
     * INTERNAL:
2463
     * Unschedules a collection from being updated when this UnitOfWork commits.
2464
     *
2465
     * @param PersistentCollectionInterface $coll
2466
     */
2467 202 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...
2468
    {
2469 202
        $oid = spl_object_hash($coll);
2470 202
        if (isset($this->collectionUpdates[$oid])) {
2471 192
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2472 192
            unset($this->collectionUpdates[$oid]);
2473 192
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2474
        }
2475 202
    }
2476
2477
    /**
2478
     * Checks whether a PersistentCollection is scheduled for update.
2479
     *
2480
     * @param PersistentCollectionInterface $coll
2481
     * @return boolean
2482
     */
2483 113
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2484
    {
2485 113
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2486
    }
2487
2488
    /**
2489
     * INTERNAL:
2490
     * Gets PersistentCollections that have been visited during computing change
2491
     * set of $document
2492
     *
2493
     * @param object $document
2494
     * @return PersistentCollectionInterface[]
2495
     */
2496 557
    public function getVisitedCollections($document)
2497
    {
2498 557
        $oid = spl_object_hash($document);
2499 557
        return isset($this->visitedCollections[$oid])
2500 226
                ? $this->visitedCollections[$oid]
2501 557
                : array();
2502
    }
2503
2504
    /**
2505
     * INTERNAL:
2506
     * Gets PersistentCollections that are scheduled to update and related to $document
2507
     *
2508
     * @param object $document
2509
     * @return array
2510
     */
2511 557
    public function getScheduledCollections($document)
2512
    {
2513 557
        $oid = spl_object_hash($document);
2514 557
        return isset($this->hasScheduledCollections[$oid])
2515 224
                ? $this->hasScheduledCollections[$oid]
2516 557
                : array();
2517
    }
2518
2519
    /**
2520
     * Checks whether the document is related to a PersistentCollection
2521
     * scheduled for update or deletion.
2522
     *
2523
     * @param object $document
2524
     * @return boolean
2525
     */
2526 44
    public function hasScheduledCollections($document)
2527
    {
2528 44
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2529
    }
2530
2531
    /**
2532
     * Marks the PersistentCollection's top-level owner as having a relation to
2533
     * a collection scheduled for update or deletion.
2534
     *
2535
     * If the owner is not scheduled for any lifecycle action, it will be
2536
     * scheduled for update to ensure that versioning takes place if necessary.
2537
     *
2538
     * If the collection is nested within atomic collection, it is immediately
2539
     * unscheduled and atomic one is scheduled for update instead. This makes
2540
     * calculating update data way easier.
2541
     *
2542
     * @param PersistentCollectionInterface $coll
2543
     */
2544 225
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2545
    {
2546 225
        $document = $this->getOwningDocument($coll->getOwner());
2547 225
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2548
2549 225
        if ($document !== $coll->getOwner()) {
2550 19
            $parent = $coll->getOwner();
2551 19
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2552 19
                list($mapping, $parent, ) = $parentAssoc;
2553
            }
2554 19
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2555 3
                $class = $this->dm->getClassMetadata(get_class($document));
2556 3
                $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...
2557 3
                $this->scheduleCollectionUpdate($atomicCollection);
2558 3
                $this->unscheduleCollectionDeletion($coll);
2559 3
                $this->unscheduleCollectionUpdate($coll);
2560
            }
2561
        }
2562
2563 225
        if ( ! $this->isDocumentScheduled($document)) {
2564 39
            $this->scheduleForUpdate($document);
2565
        }
2566 225
    }
2567
2568
    /**
2569
     * Get the top-most owning document of a given document
2570
     *
2571
     * If a top-level document is provided, that same document will be returned.
2572
     * For an embedded document, we will walk through parent associations until
2573
     * we find a top-level document.
2574
     *
2575
     * @param object $document
2576
     * @throws \UnexpectedValueException when a top-level document could not be found
2577
     * @return object
2578
     */
2579 227
    public function getOwningDocument($document)
2580
    {
2581 227
        $class = $this->dm->getClassMetadata(get_class($document));
2582 227
        while ($class->isEmbeddedDocument) {
2583 33
            $parentAssociation = $this->getParentAssociation($document);
2584
2585 33
            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...
2586
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2587
            }
2588
2589 33
            list(, $document, ) = $parentAssociation;
2590 33
            $class = $this->dm->getClassMetadata(get_class($document));
2591
        }
2592
2593 227
        return $document;
2594
    }
2595
2596
    /**
2597
     * Gets the class name for an association (embed or reference) with respect
2598
     * to any discriminator value.
2599
     *
2600
     * @param array      $mapping Field mapping for the association
2601
     * @param array|null $data    Data for the embedded document or reference
2602
     * @return string Class name.
2603
     */
2604 218
    public function getClassNameForAssociation(array $mapping, $data)
2605
    {
2606 218
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2607
2608 218
        $discriminatorValue = null;
2609 218
        if (isset($discriminatorField, $data[$discriminatorField])) {
2610 21
            $discriminatorValue = $data[$discriminatorField];
2611 198
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2612
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2613
        }
2614
2615 218
        if ($discriminatorValue !== null) {
2616 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2617 10
                ? $mapping['discriminatorMap'][$discriminatorValue]
2618 21
                : $discriminatorValue;
2619
        }
2620
2621 198
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2622
2623 198 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...
2624 15
            $discriminatorValue = $data[$class->discriminatorField];
2625 184
        } elseif ($class->defaultDiscriminatorValue !== null) {
2626 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2627
        }
2628
2629 198
        if ($discriminatorValue !== null) {
2630 16
            return isset($class->discriminatorMap[$discriminatorValue])
2631 14
                ? $class->discriminatorMap[$discriminatorValue]
2632 16
                : $discriminatorValue;
2633
        }
2634
2635 183
        return $mapping['targetDocument'];
2636
    }
2637
2638
    /**
2639
     * INTERNAL:
2640
     * Creates a document. Used for reconstitution of documents during hydration.
2641
     *
2642
     * @ignore
2643
     * @param string $className The name of the document class.
2644
     * @param array $data The data for the document.
2645
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2646
     * @param object $document The document to be hydrated into in case of creation
2647
     * @return object The document instance.
2648
     * @internal Highly performance-sensitive method.
2649
     */
2650 385
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2651
    {
2652 385
        $class = $this->dm->getClassMetadata($className);
2653
2654
        // @TODO figure out how to remove this
2655 385
        $discriminatorValue = null;
2656 385 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...
2657 19
            $discriminatorValue = $data[$class->discriminatorField];
2658 377
        } elseif (isset($class->defaultDiscriminatorValue)) {
2659 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2660
        }
2661
2662 385
        if ($discriminatorValue !== null) {
2663 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2664 18
                ? $class->discriminatorMap[$discriminatorValue]
2665 20
                : $discriminatorValue;
2666
2667 20
            $class = $this->dm->getClassMetadata($className);
2668
2669 20
            unset($data[$class->discriminatorField]);
2670
        }
2671
        
2672 385
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2673 2
            $document = $class->newInstance();
2674 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2675 2
            return $document;
2676
        }
2677
2678 384
        $isManagedObject = false;
2679 384
        if (! $class->isQueryResultDocument) {
2680 381
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2681 381
            $serializedId = serialize($id);
2682 381
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2683
        }
2684
2685 384
        if ($isManagedObject) {
2686 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...
2687 104
            $oid = spl_object_hash($document);
2688 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...
2689 14
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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