Completed
Pull Request — master (#1660)
by Andreas
26:43 queued 22:56
created

UnitOfWork::doRefresh()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4.0047

Importance

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
578 13
            && ! isset($this->documentUpserts[$oid])
579 13
            && ! isset($this->documentDeletions[$oid])
580 13
            && isset($this->documentStates[$oid])
581 13
        ) {
582 8
            $this->computeChangeSet($class, $document);
583 8
        }
584 13
    }
585
586
    /**
587
     * Gets the changeset for a document.
588
     *
589
     * @param object $document
590
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
591
     */
592 636
    public function getDocumentChangeSet($document)
593
    {
594 636
        $oid = spl_object_hash($document);
595 636
        if (isset($this->documentChangeSets[$oid])) {
596 633
            return $this->documentChangeSets[$oid];
597
        }
598 63
        return array();
599
    }
600
601
    /**
602
     * INTERNAL:
603
     * Sets the changeset for a document.
604
     *
605
     * @param object $document
606
     * @param array $changeset
607
     */
608 1
    public function setDocumentChangeSet($document, $changeset)
609
    {
610 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
611 1
    }
612
613
    /**
614
     * Get a documents actual data, flattening all the objects to arrays.
615
     *
616
     * @param object $document
617
     * @return array
618
     */
619 644
    public function getDocumentActualData($document)
620
    {
621 644
        $class = $this->dm->getClassMetadata(get_class($document));
622 644
        $actualData = array();
623 644
        foreach ($class->reflFields as $name => $refProp) {
624 644
            $mapping = $class->fieldMappings[$name];
625
            // skip not saved fields
626 644
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
627 54
                continue;
628
            }
629 644
            $value = $refProp->getValue($document);
630 644
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
631 7
                $value = new GridFSFile($value);
632 7
                $class->reflFields[$name]->setValue($document, $value);
633 7
                $actualData[$name] = $value;
634 644
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
635 644
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
636
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
637 412
                if ( ! $value instanceof Collection) {
638 147
                    $value = new ArrayCollection($value);
639 147
                }
640
641
                // Inject PersistentCollection
642 412
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
643 412
                $coll->setOwner($document, $mapping);
644 412
                $coll->setDirty( ! $value->isEmpty());
645 412
                $class->reflFields[$name]->setValue($document, $coll);
646 412
                $actualData[$name] = $coll;
647 412
            } else {
648 644
                $actualData[$name] = $value;
649
            }
650 644
        }
651 644
        return $actualData;
652
    }
653
654
    /**
655
     * Computes the changes that happened to a single document.
656
     *
657
     * Modifies/populates the following properties:
658
     *
659
     * {@link originalDocumentData}
660
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
661
     * then it was not fetched from the database and therefore we have no original
662
     * document data yet. All of the current document data is stored as the original document data.
663
     *
664
     * {@link documentChangeSets}
665
     * The changes detected on all properties of the document are stored there.
666
     * A change is a tuple array where the first entry is the old value and the second
667
     * entry is the new value of the property. Changesets are used by persisters
668
     * to INSERT/UPDATE the persistent document state.
669
     *
670
     * {@link documentUpdates}
671
     * If the document is already fully MANAGED (has been fetched from the database before)
672
     * and any changes to its properties are detected, then a reference to the document is stored
673
     * there to mark it for an update.
674
     *
675
     * @param ClassMetadata $class The class descriptor of the document.
676
     * @param object $document The document for which to compute the changes.
677
     */
678 641
    public function computeChangeSet(ClassMetadata $class, $document)
679
    {
680 641
        if ( ! $class->isInheritanceTypeNone()) {
681 196
            $class = $this->dm->getClassMetadata(get_class($document));
682 196
        }
683
684
        // Fire PreFlush lifecycle callbacks
685 641 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...
686 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
687 11
        }
688
689 641
        $this->computeOrRecomputeChangeSet($class, $document);
690 640
    }
691
692
    /**
693
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
694
     *
695
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
696
     * @param object $document
697
     * @param boolean $recompute
698
     */
699 641
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
700
    {
701 641
        $oid = spl_object_hash($document);
702 641
        $actualData = $this->getDocumentActualData($document);
703 641
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
704 641
        if ($isNewDocument) {
705
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
706
            // These result in an INSERT.
707 639
            $this->originalDocumentData[$oid] = $actualData;
708 639
            $changeSet = array();
709 639
            foreach ($actualData as $propName => $actualValue) {
710
                /* At this PersistentCollection shouldn't be here, probably it
711
                 * was cloned and its ownership must be fixed
712
                 */
713 639
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
714
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
715
                    $actualValue = $actualData[$propName];
716
                }
717
                // ignore inverse side of reference relationship
718 639 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...
719 202
                    continue;
720
                }
721 639
                $changeSet[$propName] = array(null, $actualValue);
722 639
            }
723 639
            $this->documentChangeSets[$oid] = $changeSet;
724 639
        } else {
725 301
            if ($class->isReadOnly) {
726 2
                return;
727
            }
728
            // Document is "fully" MANAGED: it was already fully persisted before
729
            // and we have a copy of the original data
730 299
            $originalData = $this->originalDocumentData[$oid];
731 299
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
732 299
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
733 2
                $changeSet = $this->documentChangeSets[$oid];
734 2
            } else {
735 299
                $changeSet = array();
736
            }
737
738 299
            foreach ($actualData as $propName => $actualValue) {
739
                // skip not saved fields
740 299
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
741
                    continue;
742
                }
743
744 299
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
745
746
                // skip if value has not changed
747 299
                if ($orgValue === $actualValue) {
748 298
                    if ($actualValue instanceof PersistentCollectionInterface) {
749 205
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
750
                            // consider dirty collections as changed as well
751 181
                            continue;
752
                        }
753 298
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
754
                        // but consider dirty GridFSFile instances as changed
755 298
                        continue;
756
                    }
757 103
                }
758
759
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
760 256
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
761 13
                    if ($orgValue !== null) {
762 8
                        $this->scheduleOrphanRemoval($orgValue);
763 8
                    }
764 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
765 13
                    continue;
766
                }
767
768
                // if owning side of reference-one relationship
769 250
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
770 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
771 1
                        $this->scheduleOrphanRemoval($orgValue);
772 1
                    }
773
774 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
775 13
                    continue;
776
                }
777
778 243
                if ($isChangeTrackingNotify) {
779 3
                    continue;
780
                }
781
782
                // ignore inverse side of reference relationship
783 241 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...
784 6
                    continue;
785
                }
786
787
                // Persistent collection was exchanged with the "originally"
788
                // created one. This can only mean it was cloned and replaced
789
                // on another document.
790 239
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
791 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
792 6
                }
793
794
                // if embed-many or reference-many relationship
795 239
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
796 119
                    $changeSet[$propName] = array($orgValue, $actualValue);
797
                    /* If original collection was exchanged with a non-empty value
798
                     * and $set will be issued, there is no need to $unset it first
799
                     */
800 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
801 28
                        continue;
802
                    }
803 99
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
804 18
                        $this->scheduleCollectionDeletion($orgValue);
805 18
                    }
806 99
                    continue;
807
                }
808
809
                // skip equivalent date values
810 157
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
811 37
                    $dateType = Type::getType('date');
812 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
813 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
814
815 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
816 30
                        continue;
817
                    }
818 10
                }
819
820
                // regular field
821 140
                $changeSet[$propName] = array($orgValue, $actualValue);
822 299
            }
823 299
            if ($changeSet) {
824 245
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
825 245
                    ? $changeSet + $this->documentChangeSets[$oid]
826 21
                    : $changeSet;
827
828 245
                $this->originalDocumentData[$oid] = $actualData;
829 245
                $this->scheduleForUpdate($document);
830 245
            }
831
        }
832
833
        // Look for changes in associations of the document
834 641
        $associationMappings = array_filter(
835 641
            $class->associationMappings,
836
            function ($assoc) { return empty($assoc['notSaved']); }
837 641
        );
838
839 641
        foreach ($associationMappings as $mapping) {
840 485
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
841
842 485
            if ($value === null) {
843 331
                continue;
844
            }
845
846 471
            $this->computeAssociationChanges($document, $mapping, $value);
847
848 470
            if (isset($mapping['reference'])) {
849 357
                continue;
850
            }
851
852 367
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
853
854 367
            foreach ($values as $obj) {
855 187
                $oid2 = spl_object_hash($obj);
856
857 187
                if (isset($this->documentChangeSets[$oid2])) {
858 185
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
859
                        // instance of $value is the same as it was previously otherwise there would be
860
                        // change set already in place
861 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
862 40
                    }
863
864 185
                    if ( ! $isNewDocument) {
865 80
                        $this->scheduleForUpdate($document);
866 80
                    }
867
868 185
                    break;
869
                }
870 367
            }
871 640
        }
872 640
    }
873
874
    /**
875
     * Computes all the changes that have been done to documents and collections
876
     * since the last commit and stores these changes in the _documentChangeSet map
877
     * temporarily for access by the persisters, until the UoW commit is finished.
878
     */
879 639
    public function computeChangeSets()
880
    {
881 639
        $this->computeScheduleInsertsChangeSets();
882 638
        $this->computeScheduleUpsertsChangeSets();
883
884
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
885 638
        foreach ($this->identityMap as $className => $documents) {
886 638
            $class = $this->dm->getClassMetadata($className);
887 638
            if ($class->isEmbeddedDocument) {
888
                /* we do not want to compute changes to embedded documents up front
889
                 * in case embedded document was replaced and its changeset
890
                 * would corrupt data. Embedded documents' change set will
891
                 * be calculated by reachability from owning document.
892
                 */
893 176
                continue;
894
            }
895
896
            // If change tracking is explicit or happens through notification, then only compute
897
            // changes on document of that type that are explicitly marked for synchronization.
898 638
            switch (true) {
899 638
                case ($class->isChangeTrackingDeferredImplicit()):
900 637
                    $documentsToProcess = $documents;
901 637
                    break;
902
903 4
                case (isset($this->scheduledForDirtyCheck[$className])):
904 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
905 3
                    break;
906
907 4
                default:
908 4
                    $documentsToProcess = array();
909
910 4
            }
911
912 638
            foreach ($documentsToProcess as $document) {
913
                // Ignore uninitialized proxy objects
914 634
                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...
915 10
                    continue;
916
                }
917
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
918 634
                $oid = spl_object_hash($document);
919 634 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...
920 634
                    && ! isset($this->documentUpserts[$oid])
921 634
                    && ! isset($this->documentDeletions[$oid])
922 634
                    && isset($this->documentStates[$oid])
923 634
                ) {
924 285
                    $this->computeChangeSet($class, $document);
925 285
                }
926 638
            }
927 638
        }
928 638
    }
929
930
    /**
931
     * Computes the changes of an association.
932
     *
933
     * @param object $parentDocument
934
     * @param array $assoc
935
     * @param mixed $value The value of the association.
936
     * @throws \InvalidArgumentException
937
     */
938 471
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
939
    {
940 471
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
941 471
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
942 471
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
943
944 471
        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...
945 8
            return;
946
        }
947
948 470
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
949 261
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
950 257
                $this->scheduleCollectionUpdate($value);
951 257
            }
952 261
            $topmostOwner = $this->getOwningDocument($value->getOwner());
953 261
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
954 261
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
955 147
                $value->initialize();
956 147
                foreach ($value->getDeletedDocuments() as $orphan) {
957 23
                    $this->scheduleOrphanRemoval($orphan);
958 147
                }
959 147
            }
960 261
        }
961
962
        // Look through the documents, and in any of their associations,
963
        // for transient (new) documents, recursively. ("Persistence by reachability")
964
        // Unwrap. Uninitialized collections will simply be empty.
965 470
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
966
967 470
        $count = 0;
968 470
        foreach ($unwrappedValue as $key => $entry) {
969 375
            if ( ! is_object($entry)) {
970 1
                throw new \InvalidArgumentException(
971 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
972 1
                );
973
            }
974
975 374
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
976
977 374
            $state = $this->getDocumentState($entry, self::STATE_NEW);
978
979
            // Handle "set" strategy for multi-level hierarchy
980 374
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
981 374
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
982
983 374
            $count++;
984
985
            switch ($state) {
986 374
                case self::STATE_NEW:
987 68
                    if ( ! $assoc['isCascadePersist']) {
988
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
989
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
990
                            . ' Explicitly persist the new document or configure cascading persist operations'
991
                            . ' on the relationship.');
992
                    }
993
994 68
                    $this->persistNew($targetClass, $entry);
995 68
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
996 68
                    $this->computeChangeSet($targetClass, $entry);
997 68
                    break;
998
999 369
                case self::STATE_MANAGED:
1000 369
                    if ($targetClass->isEmbeddedDocument) {
1001 178
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
1002 178
                        if ($knownParent && $knownParent !== $parentDocument) {
1003 9
                            $entry = clone $entry;
1004 9
                            if ($assoc['type'] === ClassMetadata::ONE) {
1005 6
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
1006 6
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
1007 6
                                $poid = spl_object_hash($parentDocument);
1008 6
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
1009 6
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
1010 6
                                }
1011 6
                            } else {
1012
                                // must use unwrapped value to not trigger orphan removal
1013 7
                                $unwrappedValue[$key] = $entry;
1014
                            }
1015 9
                            $this->persistNew($targetClass, $entry);
1016 9
                        }
1017 178
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
1018 178
                        $this->computeChangeSet($targetClass, $entry);
1019 178
                    }
1020 369
                    break;
1021
1022 1
                case self::STATE_REMOVED:
1023
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1024
                    // and remove the element from Collection.
1025 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1026
                        unset($value[$key]);
1027
                    }
1028 1
                    break;
1029
1030
                case self::STATE_DETACHED:
1031
                    // Can actually not happen right now as we assume STATE_NEW,
1032
                    // so the exception will be raised from the DBAL layer (constraint violation).
1033
                    throw new \InvalidArgumentException('A detached document was found through a '
1034
                        . 'relationship during cascading a persist operation.');
1035
1036
                default:
1037
                    // MANAGED associated documents are already taken into account
1038
                    // during changeset calculation anyway, since they are in the identity map.
1039
1040
            }
1041 469
        }
1042 469
    }
1043
1044
    /**
1045
     * INTERNAL:
1046
     * Computes the changeset of an individual document, independently of the
1047
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1048
     *
1049
     * The passed document must be a managed document. If the document already has a change set
1050
     * because this method is invoked during a commit cycle then the change sets are added.
1051
     * whereby changes detected in this method prevail.
1052
     *
1053
     * @ignore
1054
     * @param ClassMetadata $class The class descriptor of the document.
1055
     * @param object $document The document for which to (re)calculate the change set.
1056
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1057
     */
1058 21
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1059
    {
1060
        // Ignore uninitialized proxy objects
1061 21
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1892
                            // Do not merge fields marked lazy that have not been fetched
1893 1
                            continue;
1894 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1895
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1896
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1897
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1898
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1899
                                $relatedId = $targetClass->getIdentifierObject($other);
1900
1901
                                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...
1902
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1903
                                } else {
1904
                                    $other = $this
1905
                                        ->dm
1906
                                        ->getProxyFactory()
1907
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1908
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
2068 22
            } else {
2069 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2070
            }
2071 22
        }
2072
2073 22
        $this->cascadeRefresh($document, $visited);
2074 22
    }
2075
2076
    /**
2077
     * Cascades a refresh operation to associated documents.
2078
     *
2079
     * @param object $document
2080
     * @param array $visited
2081
     */
2082 22
    private function cascadeRefresh($document, array &$visited)
2083
    {
2084 22
        $class = $this->dm->getClassMetadata(get_class($document));
2085
2086 22
        $associationMappings = array_filter(
2087 22
            $class->associationMappings,
2088
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2089 22
        );
2090
2091 22
        foreach ($associationMappings as $mapping) {
2092 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2093 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2094 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2095
                    // Unwrap so that foreach() does not initialize
2096 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2097 15
                }
2098 15
                foreach ($relatedDocuments as $relatedDocument) {
2099
                    $this->doRefresh($relatedDocument, $visited);
2100 15
                }
2101 15
            } elseif ($relatedDocuments !== null) {
2102 2
                $this->doRefresh($relatedDocuments, $visited);
2103 2
            }
2104 22
        }
2105 22
    }
2106
2107
    /**
2108
     * Cascades a detach operation to associated documents.
2109
     *
2110
     * @param object $document
2111
     * @param array $visited
2112
     */
2113 17 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2114
    {
2115 17
        $class = $this->dm->getClassMetadata(get_class($document));
2116 17
        foreach ($class->fieldMappings as $mapping) {
2117 17
            if ( ! $mapping['isCascadeDetach']) {
2118 17
                continue;
2119
            }
2120 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2121 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2122 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2123
                    // Unwrap so that foreach() does not initialize
2124 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2125 8
                }
2126 11
                foreach ($relatedDocuments as $relatedDocument) {
2127 9
                    $this->doDetach($relatedDocument, $visited);
2128 11
                }
2129 11
            } elseif ($relatedDocuments !== null) {
2130 9
                $this->doDetach($relatedDocuments, $visited);
2131 9
            }
2132 17
        }
2133 17
    }
2134
    /**
2135
     * Cascades a merge operation to associated documents.
2136
     *
2137
     * @param object $document
2138
     * @param object $managedCopy
2139
     * @param array $visited
2140
     */
2141 15
    private function cascadeMerge($document, $managedCopy, array &$visited)
2142
    {
2143 15
        $class = $this->dm->getClassMetadata(get_class($document));
2144
2145 15
        $associationMappings = array_filter(
2146 15
            $class->associationMappings,
2147
            function ($assoc) { return $assoc['isCascadeMerge']; }
2148 15
        );
2149
2150 15
        foreach ($associationMappings as $assoc) {
2151 14
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2152
2153 14
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2154 10
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2155
                    // Collections are the same, so there is nothing to do
2156 1
                    continue;
2157
                }
2158
2159 10
                foreach ($relatedDocuments as $relatedDocument) {
2160 6
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2161 10
                }
2162 14
            } elseif ($relatedDocuments !== null) {
2163 5
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2164 5
            }
2165 15
        }
2166 15
    }
2167
2168
    /**
2169
     * Cascades the save operation to associated documents.
2170
     *
2171
     * @param object $document
2172
     * @param array $visited
2173
     */
2174 670
    private function cascadePersist($document, array &$visited)
2175
    {
2176 670
        $class = $this->dm->getClassMetadata(get_class($document));
2177
2178 670
        $associationMappings = array_filter(
2179 670
            $class->associationMappings,
2180
            function ($assoc) { return $assoc['isCascadePersist']; }
2181 670
        );
2182
2183 670
        foreach ($associationMappings as $fieldName => $mapping) {
2184 461
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2185
2186 461
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2187 382
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2188 17
                    if ($relatedDocuments->getOwner() !== $document) {
2189 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2190 2
                    }
2191
                    // Unwrap so that foreach() does not initialize
2192 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2193 17
                }
2194
2195 382
                $count = 0;
2196 382
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2197 206
                    if ( ! empty($mapping['embedded'])) {
2198 126
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2199 126
                        if ($knownParent && $knownParent !== $document) {
2200 4
                            $relatedDocument = clone $relatedDocument;
2201 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2202 4
                        }
2203 126
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2204 126
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2205 126
                    }
2206 206
                    $this->doPersist($relatedDocument, $visited);
2207 381
                }
2208 461
            } elseif ($relatedDocuments !== null) {
2209 138
                if ( ! empty($mapping['embedded'])) {
2210 77
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2211 77
                    if ($knownParent && $knownParent !== $document) {
2212 6
                        $relatedDocuments = clone $relatedDocuments;
2213 6
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2214 6
                    }
2215 77
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2216 77
                }
2217 138
                $this->doPersist($relatedDocuments, $visited);
2218 137
            }
2219 669
        }
2220 668
    }
2221
2222
    /**
2223
     * Cascades the delete operation to associated documents.
2224
     *
2225
     * @param object $document
2226
     * @param array $visited
2227
     */
2228 77 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...
2229
    {
2230 77
        $class = $this->dm->getClassMetadata(get_class($document));
2231 77
        foreach ($class->fieldMappings as $mapping) {
2232 77
            if ( ! $mapping['isCascadeRemove']) {
2233 76
                continue;
2234
            }
2235 36
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
2336
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2337
        }
2338 422
    }
2339
2340
    /**
2341
     * INTERNAL:
2342
     * Schedules an embedded document for removal. The remove() operation will be
2343
     * invoked on that document at the beginning of the next commit of this
2344
     * UnitOfWork.
2345
     *
2346
     * @ignore
2347
     * @param object $document
2348
     */
2349 53
    public function scheduleOrphanRemoval($document)
2350
    {
2351 53
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2352 53
    }
2353
2354
    /**
2355
     * INTERNAL:
2356
     * Unschedules an embedded or referenced object for removal.
2357
     *
2358
     * @ignore
2359
     * @param object $document
2360
     */
2361 114
    public function unscheduleOrphanRemoval($document)
2362
    {
2363 114
        $oid = spl_object_hash($document);
2364 114
        if (isset($this->orphanRemovals[$oid])) {
2365 1
            unset($this->orphanRemovals[$oid]);
2366 1
        }
2367 114
    }
2368
2369
    /**
2370
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2371
     *  1) sets owner if it was cloned
2372
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2373
     *  3) NOP if state is OK
2374
     * Returned collection should be used from now on (only important with 2nd point)
2375
     *
2376
     * @param PersistentCollectionInterface $coll
2377
     * @param object $document
2378
     * @param ClassMetadata $class
2379
     * @param string $propName
2380
     * @return PersistentCollectionInterface
2381
     */
2382 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2383
    {
2384 8
        $owner = $coll->getOwner();
2385 8
        if ($owner === null) { // cloned
2386 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2387 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2388 2
            if ( ! $coll->isInitialized()) {
2389 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2390 1
            }
2391 2
            $newValue = clone $coll;
2392 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2393 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2394 2
            if ($this->isScheduledForUpdate($document)) {
2395
                // @todo following line should be superfluous once collections are stored in change sets
2396
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2397
            }
2398 2
            return $newValue;
2399
        }
2400 6
        return $coll;
2401
    }
2402
2403
    /**
2404
     * INTERNAL:
2405
     * Schedules a complete collection for removal when this UnitOfWork commits.
2406
     *
2407
     * @param PersistentCollectionInterface $coll
2408
     */
2409 43
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2410
    {
2411 43
        $oid = spl_object_hash($coll);
2412 43
        unset($this->collectionUpdates[$oid]);
2413 43
        if ( ! isset($this->collectionDeletions[$oid])) {
2414 43
            $this->collectionDeletions[$oid] = $coll;
2415 43
            $this->scheduleCollectionOwner($coll);
2416 43
        }
2417 43
    }
2418
2419
    /**
2420
     * Checks whether a PersistentCollection is scheduled for deletion.
2421
     *
2422
     * @param PersistentCollectionInterface $coll
2423
     * @return boolean
2424
     */
2425 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2426
    {
2427 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2428
    }
2429
2430
    /**
2431
     * INTERNAL:
2432
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2433
     *
2434
     * @param PersistentCollectionInterface $coll
2435
     */
2436 235 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...
2437
    {
2438 235
        $oid = spl_object_hash($coll);
2439 235
        if (isset($this->collectionDeletions[$oid])) {
2440 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2441 12
            unset($this->collectionDeletions[$oid]);
2442 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2443 12
        }
2444 235
    }
2445
2446
    /**
2447
     * INTERNAL:
2448
     * Schedules a collection for update when this UnitOfWork commits.
2449
     *
2450
     * @param PersistentCollectionInterface $coll
2451
     */
2452 257
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2453
    {
2454 257
        $mapping = $coll->getMapping();
2455 257
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2456
            /* There is no need to $unset collection if it will be $set later
2457
             * This is NOP if collection is not scheduled for deletion
2458
             */
2459 41
            $this->unscheduleCollectionDeletion($coll);
2460 41
        }
2461 257
        $oid = spl_object_hash($coll);
2462 257
        if ( ! isset($this->collectionUpdates[$oid])) {
2463 257
            $this->collectionUpdates[$oid] = $coll;
2464 257
            $this->scheduleCollectionOwner($coll);
2465 257
        }
2466 257
    }
2467
2468
    /**
2469
     * INTERNAL:
2470
     * Unschedules a collection from being updated when this UnitOfWork commits.
2471
     *
2472
     * @param PersistentCollectionInterface $coll
2473
     */
2474 235 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...
2475
    {
2476 235
        $oid = spl_object_hash($coll);
2477 235
        if (isset($this->collectionUpdates[$oid])) {
2478 225
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2479 225
            unset($this->collectionUpdates[$oid]);
2480 225
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2481 225
        }
2482 235
    }
2483
2484
    /**
2485
     * Checks whether a PersistentCollection is scheduled for update.
2486
     *
2487
     * @param PersistentCollectionInterface $coll
2488
     * @return boolean
2489
     */
2490 133
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2491
    {
2492 133
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2493
    }
2494
2495
    /**
2496
     * INTERNAL:
2497
     * Gets PersistentCollections that have been visited during computing change
2498
     * set of $document
2499
     *
2500
     * @param object $document
2501
     * @return PersistentCollectionInterface[]
2502
     */
2503 620
    public function getVisitedCollections($document)
2504
    {
2505 620
        $oid = spl_object_hash($document);
2506 620
        return isset($this->visitedCollections[$oid])
2507 620
                ? $this->visitedCollections[$oid]
2508 620
                : array();
2509
    }
2510
2511
    /**
2512
     * INTERNAL:
2513
     * Gets PersistentCollections that are scheduled to update and related to $document
2514
     *
2515
     * @param object $document
2516
     * @return array
2517
     */
2518 621
    public function getScheduledCollections($document)
2519
    {
2520 621
        $oid = spl_object_hash($document);
2521 621
        return isset($this->hasScheduledCollections[$oid])
2522 621
                ? $this->hasScheduledCollections[$oid]
2523 621
                : array();
2524
    }
2525
2526
    /**
2527
     * Checks whether the document is related to a PersistentCollection
2528
     * scheduled for update or deletion.
2529
     *
2530
     * @param object $document
2531
     * @return boolean
2532
     */
2533 52
    public function hasScheduledCollections($document)
2534
    {
2535 52
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2536
    }
2537
2538
    /**
2539
     * Marks the PersistentCollection's top-level owner as having a relation to
2540
     * a collection scheduled for update or deletion.
2541
     *
2542
     * If the owner is not scheduled for any lifecycle action, it will be
2543
     * scheduled for update to ensure that versioning takes place if necessary.
2544
     *
2545
     * If the collection is nested within atomic collection, it is immediately
2546
     * unscheduled and atomic one is scheduled for update instead. This makes
2547
     * calculating update data way easier.
2548
     *
2549
     * @param PersistentCollectionInterface $coll
2550
     */
2551 259
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2552
    {
2553 259
        $document = $this->getOwningDocument($coll->getOwner());
2554 259
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2555
2556 259
        if ($document !== $coll->getOwner()) {
2557 25
            $parent = $coll->getOwner();
2558 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2559 25
                list($mapping, $parent, ) = $parentAssoc;
2560 25
            }
2561 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2562 8
                $class = $this->dm->getClassMetadata(get_class($document));
2563 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
0 ignored issues
show
Bug introduced by
The variable $mapping does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

Loading history...
2593
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2594
            }
2595
2596 40
            list(, $document, ) = $parentAssociation;
2597 40
            $class = $this->dm->getClassMetadata(get_class($document));
2598 40
        }
2599
2600 261
        return $document;
2601
    }
2602
2603
    /**
2604
     * Gets the class name for an association (embed or reference) with respect
2605
     * to any discriminator value.
2606
     *
2607
     * @param array      $mapping Field mapping for the association
2608
     * @param array|null $data    Data for the embedded document or reference
2609
     * @return string Class name.
2610
     */
2611 236
    public function getClassNameForAssociation(array $mapping, $data)
2612
    {
2613 236
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2614
2615 236
        $discriminatorValue = null;
2616 236
        if (isset($discriminatorField, $data[$discriminatorField])) {
2617 21
            $discriminatorValue = $data[$discriminatorField];
2618 236
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2619
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2620
        }
2621
2622 236
        if ($discriminatorValue !== null) {
2623 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2624 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2625 21
                : $discriminatorValue;
2626
        }
2627
2628 216
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2629
2630 216 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...
2631 15
            $discriminatorValue = $data[$class->discriminatorField];
2632 216
        } elseif ($class->defaultDiscriminatorValue !== null) {
2633 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2634 1
        }
2635
2636 216
        if ($discriminatorValue !== null) {
2637 16
            return isset($class->discriminatorMap[$discriminatorValue])
2638 16
                ? $class->discriminatorMap[$discriminatorValue]
2639 16
                : $discriminatorValue;
2640
        }
2641
2642 200
        return $mapping['targetDocument'];
2643
    }
2644
2645
    /**
2646
     * INTERNAL:
2647
     * Creates a document. Used for reconstitution of documents during hydration.
2648
     *
2649
     * @ignore
2650
     * @param string $className The name of the document class.
2651
     * @param array $data The data for the document.
2652
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2653
     * @param object $document The document to be hydrated into in case of creation
2654
     * @return object The document instance.
2655
     * @internal Highly performance-sensitive method.
2656
     */
2657 427
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2658
    {
2659 427
        $class = $this->dm->getClassMetadata($className);
2660
2661
        // @TODO figure out how to remove this
2662 427
        $discriminatorValue = null;
2663 427 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...
2664 19
            $discriminatorValue = $data[$class->discriminatorField];
2665 427
        } elseif (isset($class->defaultDiscriminatorValue)) {
2666 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2667 2
        }
2668
2669 427
        if ($discriminatorValue !== null) {
2670 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2671 20
                ? $class->discriminatorMap[$discriminatorValue]
2672 20
                : $discriminatorValue;
2673
2674 20
            $class = $this->dm->getClassMetadata($className);
2675
2676 20
            unset($data[$class->discriminatorField]);
2677 20
        }
2678
        
2679 427
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2680 2
            $document = $class->newInstance();
2681 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2682 2
            return $document;
2683
        }
2684
2685 426
        $isManagedObject = false;
2686 426
        if (! $class->isQueryResultDocument) {
2687 426
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2688 426
            $serializedId = serialize($id);
2689 426
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2690 426
        }
2691
2692 426
        if ($isManagedObject) {
2693 111
            $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...
2694 111
            $oid = spl_object_hash($document);
2695 111
            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...
2696 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...
2697 14
                $overrideLocalValues = true;
2698 14
                if ($document instanceof NotifyPropertyChanged) {
2699
                    $document->addPropertyChangedListener($this);
2700
                }
2701 14
            } else {
2702 107
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2703
            }
2704 111
            if ($overrideLocalValues) {
2705 52
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2706 52
                $this->originalDocumentData[$oid] = $data;
2707 52
            }
2708 111
        } else {
2709 384
            if ($document === null) {
2710 384
                $document = $class->newInstance();
2711 384
            }
2712
2713 384
            if (! $class->isQueryResultDocument) {
2714 383
                $this->registerManaged($document, $id, $data);
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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