Completed
Pull Request — master (#1640)
by Olivier
23:26 queued 21:41
created

UnitOfWork::getCollectionPersister()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
406 630
        ) {
407 25
            return; // Nothing to do.
408
        }
409
410 627
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
411 50
            foreach ($this->orphanRemovals as $removal) {
412 50
                $this->remove($removal);
413 50
            }
414 50
        }
415
416
        // Raise onFlush
417 627
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419 7
        }
420
421 627
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 89
            list($class, $documents) = $classAndDocuments;
423 89
            $this->executeUpserts($class, $documents, $options);
424 627
        }
425
426 627
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 547
            list($class, $documents) = $classAndDocuments;
428 547
            $this->executeInserts($class, $documents, $options);
429 626
        }
430
431 626
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 237
            list($class, $documents) = $classAndDocuments;
433 237
            $this->executeUpdates($class, $documents, $options);
434 625
        }
435
436 625
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 72
            list($class, $documents) = $classAndDocuments;
438 72
            $this->executeDeletions($class, $documents, $options);
439 625
        }
440
441
        // Raise postFlush
442 625
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 625
        $this->documentInsertions =
448 625
        $this->documentUpserts =
449 625
        $this->documentUpdates =
450 625
        $this->documentDeletions =
451 625
        $this->documentChangeSets =
452 625
        $this->collectionUpdates =
453 625
        $this->collectionDeletions =
454 625
        $this->visitedCollections =
455 625
        $this->scheduledForDirtyCheck =
456 625
        $this->orphanRemovals =
457 625
        $this->hasScheduledCollections = array();
458 625
    }
459
460
    /**
461
     * Groups a list of scheduled documents by their class.
462
     *
463
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
464
     * @param bool $includeEmbedded
465
     * @return array Tuples of ClassMetadata and a corresponding array of objects
466
     */
467 627
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 627
        if (empty($documents)) {
470 627
            return array();
471
        }
472 626
        $divided = array();
473 626
        $embeds = array();
474 626
        foreach ($documents as $oid => $d) {
475 626
            $className = get_class($d);
476 626
            if (isset($embeds[$className])) {
477 78
                continue;
478
            }
479 626
            if (isset($divided[$className])) {
480 160
                $divided[$className][1][$oid] = $d;
481 160
                continue;
482
            }
483 626
            $class = $this->dm->getClassMetadata($className);
484 626
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 183
                $embeds[$className] = true;
486 183
                continue;
487
            }
488 626
            if (empty($divided[$class->name])) {
489 626
                $divided[$class->name] = array($class, array($oid => $d));
490 626
            } else {
491 4
                $divided[$class->name][1][$oid] = $d;
492
            }
493 626
        }
494 626
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 634 View Code Duplication
    private function computeScheduleInsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
503
    {
504 634
        foreach ($this->documentInsertions as $document) {
505 558
            $class = $this->dm->getClassMetadata(get_class($document));
506 558
            if ( ! $class->isEmbeddedDocument) {
507 552
                $this->computeChangeSet($class, $document);
508 551
            }
509 633
        }
510 633
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 633 View Code Duplication
    private function computeScheduleUpsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
518
    {
519 633
        foreach ($this->documentUpserts as $document) {
520 88
            $class = $this->dm->getClassMetadata(get_class($document));
521 88
            if ( ! $class->isEmbeddedDocument) {
522 88
                $this->computeChangeSet($class, $document);
523 88
            }
524 633
        }
525 633
    }
526
527
    /**
528
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
529
     *
530
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
531
     * 2. Proxies are skipped.
532
     * 3. Only if document is properly managed.
533
     *
534
     * @param  object $document
535
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
536
     * @return void
537
     */
538 14
    private function computeSingleDocumentChangeSet($document)
539
    {
540 14
        $state = $this->getDocumentState($document);
541
542 14
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
543 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
544
        }
545
546 13
        $class = $this->dm->getClassMetadata(get_class($document));
547
548 13
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
549 10
            $this->persist($document);
550 10
        }
551
552
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
553 13
        $this->computeScheduleInsertsChangeSets();
554 13
        $this->computeScheduleUpsertsChangeSets();
555
556
        // Ignore uninitialized proxy objects
557 13
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
565 13
            && ! isset($this->documentUpserts[$oid])
566 13
            && ! isset($this->documentDeletions[$oid])
567 13
            && isset($this->documentStates[$oid])
568 13
        ) {
569 8
            $this->computeChangeSet($class, $document);
570 8
        }
571 13
    }
572
573
    /**
574
     * Gets the changeset for a document.
575
     *
576
     * @param object $document
577
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
578
     */
579 627
    public function getDocumentChangeSet($document)
580
    {
581 627
        $oid = spl_object_hash($document);
582 627
        if (isset($this->documentChangeSets[$oid])) {
583 624
            return $this->documentChangeSets[$oid];
584
        }
585 62
        return array();
586
    }
587
588
    /**
589
     * INTERNAL:
590
     * Sets the changeset for a document.
591
     *
592
     * @param object $document
593
     * @param array $changeset
594
     */
595 1
    public function setDocumentChangeSet($document, $changeset)
596
    {
597 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
598 1
    }
599
600
    /**
601
     * Get a documents actual data, flattening all the objects to arrays.
602
     *
603
     * @param object $document
604
     * @return array
605
     */
606 634
    public function getDocumentActualData($document)
607
    {
608 634
        $class = $this->dm->getClassMetadata(get_class($document));
609 634
        $actualData = array();
610 634
        foreach ($class->reflFields as $name => $refProp) {
611 634
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 634
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 54
                continue;
615
            }
616 634
            $value = $refProp->getValue($document);
617 634
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
618 7
                $value = new GridFSFile($value);
619 7
                $class->reflFields[$name]->setValue($document, $value);
620 7
                $actualData[$name] = $value;
621 634
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 634
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 408
                if ( ! $value instanceof Collection) {
625 144
                    $value = new ArrayCollection($value);
626 144
                }
627
628
                // Inject PersistentCollection
629 408
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
630 408
                $coll->setOwner($document, $mapping);
631 408
                $coll->setDirty( ! $value->isEmpty());
632 408
                $class->reflFields[$name]->setValue($document, $coll);
633 408
                $actualData[$name] = $coll;
634 408
            } else {
635 634
                $actualData[$name] = $value;
636
            }
637 634
        }
638 634
        return $actualData;
639
    }
640
641
    /**
642
     * Computes the changes that happened to a single document.
643
     *
644
     * Modifies/populates the following properties:
645
     *
646
     * {@link originalDocumentData}
647
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
648
     * then it was not fetched from the database and therefore we have no original
649
     * document data yet. All of the current document data is stored as the original document data.
650
     *
651
     * {@link documentChangeSets}
652
     * The changes detected on all properties of the document are stored there.
653
     * A change is a tuple array where the first entry is the old value and the second
654
     * entry is the new value of the property. Changesets are used by persisters
655
     * to INSERT/UPDATE the persistent document state.
656
     *
657
     * {@link documentUpdates}
658
     * If the document is already fully MANAGED (has been fetched from the database before)
659
     * and any changes to its properties are detected, then a reference to the document is stored
660
     * there to mark it for an update.
661
     *
662
     * @param ClassMetadata $class The class descriptor of the document.
663
     * @param object $document The document for which to compute the changes.
664
     */
665 631
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 631
        if ( ! $class->isInheritanceTypeNone()) {
668 192
            $class = $this->dm->getClassMetadata(get_class($document));
669 192
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 631 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
673 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
674 11
        }
675
676 631
        $this->computeOrRecomputeChangeSet($class, $document);
677 630
    }
678
679
    /**
680
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
681
     *
682
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
683
     * @param object $document
684
     * @param boolean $recompute
685
     */
686 631
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 631
        $oid = spl_object_hash($document);
689 631
        $actualData = $this->getDocumentActualData($document);
690 631
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 631
        if ($isNewDocument) {
692
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
693
            // These result in an INSERT.
694 629
            $this->originalDocumentData[$oid] = $actualData;
695 629
            $changeSet = array();
696 629
            foreach ($actualData as $propName => $actualValue) {
697
                /* At this PersistentCollection shouldn't be here, probably it
698
                 * was cloned and its ownership must be fixed
699
                 */
700 629
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
701
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
702
                    $actualValue = $actualData[$propName];
703
                }
704
                // ignore inverse side of reference relationship
705 629 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1832
                    $managedCopy->__load();
1833
                }
1834 12
            }
1835
1836 15
            if ($managedCopy === null) {
1837
                // Create a new managed instance
1838 7
                $managedCopy = $class->newInstance();
1839 7
                if ($id !== null) {
1840 3
                    $class->setIdentifierValue($managedCopy, $id);
1841 3
                }
1842 7
                $this->persistNew($class, $managedCopy);
1843 7
            }
1844
1845 15
            if ($class->isVersioned) {
1846
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1847
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1848
1849
                // Throw exception if versions don't match
1850
                if ($managedCopyVersion != $documentVersion) {
1851
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1852
                }
1853
            }
1854
1855
            // Merge state of $document into existing (managed) document
1856 15
            foreach ($class->reflClass->getProperties() as $prop) {
1857 15
                $name = $prop->name;
1858 15
                $prop->setAccessible(true);
1859 15
                if ( ! isset($class->associationMappings[$name])) {
1860 15
                    if ( ! $class->isIdentifier($name)) {
1861 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1862 15
                    }
1863 15
                } else {
1864 15
                    $assoc2 = $class->associationMappings[$name];
1865
1866 15
                    if ($assoc2['type'] === 'one') {
1867 7
                        $other = $prop->getValue($document);
1868
1869 7
                        if ($other === null) {
1870 2
                            $prop->setValue($managedCopy, null);
1871 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...
1872
                            // Do not merge fields marked lazy that have not been fetched
1873 1
                            continue;
1874 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1875
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1876
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1877
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1878
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1879
                                $relatedId = $targetClass->getIdentifierObject($other);
1880
1881
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1882
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1883
                                } else {
1884
                                    $other = $this
1885
                                        ->dm
1886
                                        ->getProxyFactory()
1887
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1888
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

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

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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