Completed
Pull Request — master (#1437)
by Maciej
10:13
created

UnitOfWork::fixPersistentCollectionOwnership()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.009

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 13
cts 14
cp 0.9286
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 14
nc 6
nop 4
crap 5.009
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 1046
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
271
    {
272 1046
        $this->dm = $dm;
273 1046
        $this->evm = $evm;
274 1046
        $this->hydratorFactory = $hydratorFactory;
275 1046
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
276 1046
    }
277
278
    /**
279
     * Factory for returning new PersistenceBuilder instances used for preparing data into
280
     * queries for insert persistence.
281
     *
282
     * @return PersistenceBuilder $pb
283
     */
284 729
    public function getPersistenceBuilder()
285
    {
286 729
        if ( ! $this->persistenceBuilder) {
287 729
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
288
        }
289 729
        return $this->persistenceBuilder;
290
    }
291
292
    /**
293
     * Sets the parent association for a given embedded document.
294
     *
295
     * @param object $document
296
     * @param array $mapping
297
     * @param object $parent
298
     * @param string $propertyPath
299
     */
300 199
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
301
    {
302 199
        $oid = spl_object_hash($document);
303 199
        $this->embeddedDocumentsRegistry[$oid] = $document;
304 199
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
305 199
    }
306
307
    /**
308
     * Gets the parent association for a given embedded document.
309
     *
310
     *     <code>
311
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
312
     *     </code>
313
     *
314
     * @param object $document
315
     * @return array $association
316
     */
317 229
    public function getParentAssociation($document)
318
    {
319 229
        $oid = spl_object_hash($document);
320 229
        if ( ! isset($this->parentAssociations[$oid])) {
321 224
            return null;
322
        }
323 178
        return $this->parentAssociations[$oid];
324
    }
325
326
    /**
327
     * Get the document persister instance for the given document name
328
     *
329
     * @param string $documentName
330
     * @return Persisters\DocumentPersister
331
     */
332 727
    public function getDocumentPersister($documentName)
333
    {
334 727
        if ( ! isset($this->persisters[$documentName])) {
335 713
            $class = $this->dm->getClassMetadata($documentName);
336 713
            $pb = $this->getPersistenceBuilder();
337 713
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
338
        }
339 727
        return $this->persisters[$documentName];
340
    }
341
342
    /**
343
     * Get the collection persister instance.
344
     *
345
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
346
     */
347 727
    public function getCollectionPersister()
348
    {
349 727
        if ( ! isset($this->collectionPersister)) {
350 727
            $pb = $this->getPersistenceBuilder();
351 727
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
352
        }
353 727
        return $this->collectionPersister;
354
    }
355
356
    /**
357
     * Set the document persister instance to use for the given document name
358
     *
359
     * @param string $documentName
360
     * @param Persisters\DocumentPersister $persister
361
     */
362 14
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
363
    {
364 14
        $this->persisters[$documentName] = $persister;
365 14
    }
366
367
    /**
368
     * Commits the UnitOfWork, executing all operations that have been postponed
369
     * up to this point. The state of all managed documents will be synchronized with
370
     * the database.
371
     *
372
     * The operations are executed in the following order:
373
     *
374
     * 1) All document insertions
375
     * 2) All document updates
376
     * 3) All document deletions
377
     *
378
     * @param object $document
379
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
380
     */
381 607
    public function commit($document = null, array $options = array())
382
    {
383
        // Raise preFlush
384 607
        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 607
        if ($document === null) {
390 601
            $this->computeChangeSets();
391 13
        } elseif (is_object($document)) {
392 12
            $this->computeSingleDocumentChangeSet($document);
393 1
        } elseif (is_array($document)) {
394 1
            foreach ($document as $object) {
395 1
                $this->computeSingleDocumentChangeSet($object);
396
            }
397
        }
398
399 605
        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 260
            $this->documentUpserts ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
411 48
            foreach ($this->orphanRemovals as $removal) {
412 48
                $this->remove($removal);
413
            }
414
        }
415
416
        // Raise onFlush
417 602
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419
        }
420
421 602
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 85
            list($class, $documents) = $classAndDocuments;
423 85
            $this->executeUpserts($class, $documents, $options);
424
        }
425
426 602
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 528
            list($class, $documents) = $classAndDocuments;
428 528
            $this->executeInserts($class, $documents, $options);
429
        }
430
431 601
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 232
            list($class, $documents) = $classAndDocuments;
433 232
            $this->executeUpdates($class, $documents, $options);
434
        }
435
436 601
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 70
            list($class, $documents) = $classAndDocuments;
438 70
            $this->executeDeletions($class, $documents, $options);
439
        }
440
441
        // Raise postFlush
442 601
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 601
        $this->documentInsertions =
448 601
        $this->documentUpserts =
449 601
        $this->documentUpdates =
450 601
        $this->documentDeletions =
451 601
        $this->documentChangeSets =
452 601
        $this->collectionUpdates =
453 601
        $this->collectionDeletions =
454 601
        $this->visitedCollections =
455 601
        $this->scheduledForDirtyCheck =
456 601
        $this->orphanRemovals =
457 601
        $this->hasScheduledCollections = array();
458 601
    }
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 602
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 602
        if (empty($documents)) {
470 602
            return array();
471
        }
472 601
        $divided = array();
473 601
        $embeds = array();
474 601
        foreach ($documents as $oid => $d) {
475 601
            $className = get_class($d);
476 601
            if (isset($embeds[$className])) {
477 77
                continue;
478
            }
479 601
            if (isset($divided[$className])) {
480 146
                $divided[$className][1][$oid] = $d;
481 146
                continue;
482
            }
483 601
            $class = $this->dm->getClassMetadata($className);
484 601
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 180
                $embeds[$className] = true;
486 180
                continue;
487
            }
488 601
            if (empty($divided[$class->name])) {
489 601
                $divided[$class->name] = array($class, array($oid => $d));
490
            } else {
491 601
                $divided[$class->name][1][$oid] = $d;
492
            }
493
        }
494 601
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 609 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 609
        foreach ($this->documentInsertions as $document) {
505 538
            $class = $this->dm->getClassMetadata(get_class($document));
506 538
            if ( ! $class->isEmbeddedDocument) {
507 538
                $this->computeChangeSet($class, $document);
508
            }
509
        }
510 608
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 608 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 608
        foreach ($this->documentUpserts as $document) {
520 84
            $class = $this->dm->getClassMetadata(get_class($document));
521 84
            if ( ! $class->isEmbeddedDocument) {
522 84
                $this->computeChangeSet($class, $document);
523
            }
524
        }
525 608
    }
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 13
    private function computeSingleDocumentChangeSet($document)
539
    {
540 13
        $state = $this->getDocumentState($document);
541
542 13
        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 12
        $class = $this->dm->getClassMetadata(get_class($document));
547
548 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
549 9
            $this->persist($document);
550
        }
551
552
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
553 12
        $this->computeScheduleInsertsChangeSets();
554 12
        $this->computeScheduleUpsertsChangeSets();
555
556
        // Ignore uninitialized proxy objects
557 12
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
565 12
            && ! isset($this->documentUpserts[$oid])
566 12
            && ! isset($this->documentDeletions[$oid])
567 12
            && isset($this->documentStates[$oid])
568
        ) {
569 8
            $this->computeChangeSet($class, $document);
570
        }
571 12
    }
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 602
    public function getDocumentChangeSet($document)
580
    {
581 602
        $oid = spl_object_hash($document);
582 602
        if (isset($this->documentChangeSets[$oid])) {
583 599
            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 609
    public function getDocumentActualData($document)
607
    {
608 609
        $class = $this->dm->getClassMetadata(get_class($document));
609 609
        $actualData = array();
610 609
        foreach ($class->reflFields as $name => $refProp) {
611 609
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 609
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 51
                continue;
615
            }
616 609
            $value = $refProp->getValue($document);
617 609
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
618 6
                $value = new GridFSFile($value);
619 6
                $class->reflFields[$name]->setValue($document, $value);
620 6
                $actualData[$name] = $value;
621 609
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 609
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 391
                if ( ! $value instanceof Collection) {
625 130
                    $value = new ArrayCollection($value);
626
                }
627
628
                // Inject PersistentCollection
629 391
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
630 391
                $coll->setOwner($document, $mapping);
631 391
                $coll->setDirty( ! $value->isEmpty());
632 391
                $class->reflFields[$name]->setValue($document, $coll);
633 391
                $actualData[$name] = $coll;
634
            } else {
635 609
                $actualData[$name] = $value;
636
            }
637
        }
638 609
        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 606
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 606
        if ( ! $class->isInheritanceTypeNone()) {
668 180
            $class = $this->dm->getClassMetadata(get_class($document));
669
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 606 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
673 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
674
        }
675
676 606
        $this->computeOrRecomputeChangeSet($class, $document);
677 605
    }
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 606
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 606
        $oid = spl_object_hash($document);
689 606
        $actualData = $this->getDocumentActualData($document);
690 606
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 606
        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 606
            $this->originalDocumentData[$oid] = $actualData;
695 606
            $changeSet = array();
696 606
            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 606
                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 606 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 184
                    continue;
707
                }
708 606
                $changeSet[$propName] = array(null, $actualValue);
709
            }
710 606
            $this->documentChangeSets[$oid] = $changeSet;
711
        } else {
712
            // Document is "fully" MANAGED: it was already fully persisted before
713
            // and we have a copy of the original data
714 292
            $originalData = $this->originalDocumentData[$oid];
715 292
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
716 292
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
717 2
                $changeSet = $this->documentChangeSets[$oid];
718
            } else {
719 292
                $changeSet = array();
720
            }
721
722 292
            foreach ($actualData as $propName => $actualValue) {
723
                // skip not saved fields
724 292
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
725
                    continue;
726
                }
727
728 292
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
729
730
                // skip if value has not changed
731 292
                if ($orgValue === $actualValue) {
732 291
                    if ($actualValue instanceof PersistentCollectionInterface) {
733 203
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
734
                            // consider dirty collections as changed as well
735 203
                            continue;
736
                        }
737 291
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
738
                        // but consider dirty GridFSFile instances as changed
739 291
                        continue;
740
                    }
741
                }
742
743
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
744 251
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
745 12
                    if ($orgValue !== null) {
746 8
                        $this->scheduleOrphanRemoval($orgValue);
747
                    }
748
749 12
                    $changeSet[$propName] = array($orgValue, $actualValue);
750 12
                    continue;
751
                }
752
753
                // if owning side of reference-one relationship
754 245
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
755 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
756 1
                        $this->scheduleOrphanRemoval($orgValue);
757
                    }
758
759 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
760 13
                    continue;
761
                }
762
763 238
                if ($isChangeTrackingNotify) {
764 3
                    continue;
765
                }
766
767
                // ignore inverse side of reference relationship
768 236 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
769 6
                    continue;
770
                }
771
772
                // Persistent collection was exchanged with the "originally"
773
                // created one. This can only mean it was cloned and replaced
774
                // on another document.
775 234
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
776 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
777
                }
778
779
                // if embed-many or reference-many relationship
780 234
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
781 117
                    $changeSet[$propName] = array($orgValue, $actualValue);
782
                    /* If original collection was exchanged with a non-empty value
783
                     * and $set will be issued, there is no need to $unset it first
784
                     */
785 117
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
786 28
                        continue;
787
                    }
788 97
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
789 18
                        $this->scheduleCollectionDeletion($orgValue);
790
                    }
791 97
                    continue;
792
                }
793
794
                // skip equivalent date values
795 154
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
796 37
                    $dateType = Type::getType('date');
797 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
798 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
799
800 37
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
801 30
                        continue;
802
                    }
803
                }
804
805
                // regular field
806 137
                $changeSet[$propName] = array($orgValue, $actualValue);
807
            }
808 292
            if ($changeSet) {
809 240
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
810 21
                    ? $changeSet + $this->documentChangeSets[$oid]
811 235
                    : $changeSet;
812
813 240
                $this->originalDocumentData[$oid] = $actualData;
814 240
                $this->scheduleForUpdate($document);
815
            }
816
        }
817
818
        // Look for changes in associations of the document
819 606
        $associationMappings = array_filter(
820 606
            $class->associationMappings,
821
            function ($assoc) { return empty($assoc['notSaved']); }
822
        );
823
824 606
        foreach ($associationMappings as $mapping) {
825 461
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
826
827 461
            if ($value === null) {
828 309
                continue;
829
            }
830
831 448
            $this->computeAssociationChanges($document, $mapping, $value);
832
833 447
            if (isset($mapping['reference'])) {
834 337
                continue;
835
            }
836
837 349
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
838
839 349
            foreach ($values as $obj) {
840 184
                $oid2 = spl_object_hash($obj);
841
842 184
                if (isset($this->documentChangeSets[$oid2])) {
843 182
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
844
                        // instance of $value is the same as it was previously otherwise there would be
845
                        // change set already in place
846 40
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
847
                    }
848
849 182
                    if ( ! $isNewDocument) {
850 79
                        $this->scheduleForUpdate($document);
851
                    }
852
853 349
                    break;
854
                }
855
            }
856
        }
857 605
    }
858
859
    /**
860
     * Computes all the changes that have been done to documents and collections
861
     * since the last commit and stores these changes in the _documentChangeSet map
862
     * temporarily for access by the persisters, until the UoW commit is finished.
863
     */
864 604
    public function computeChangeSets()
865
    {
866 604
        $this->computeScheduleInsertsChangeSets();
867 603
        $this->computeScheduleUpsertsChangeSets();
868
869
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
870 603
        foreach ($this->identityMap as $className => $documents) {
871 603
            $class = $this->dm->getClassMetadata($className);
872 603
            if ($class->isEmbeddedDocument) {
873
                /* we do not want to compute changes to embedded documents up front
874
                 * in case embedded document was replaced and its changeset
875
                 * would corrupt data. Embedded documents' change set will
876
                 * be calculated by reachability from owning document.
877
                 */
878 173
                continue;
879
            }
880
881
            // If change tracking is explicit or happens through notification, then only compute
882
            // changes on document of that type that are explicitly marked for synchronization.
883
            switch (true) {
884 603
                case ($class->isChangeTrackingDeferredImplicit()):
885 602
                    $documentsToProcess = $documents;
886 602
                    break;
887
888 4
                case (isset($this->scheduledForDirtyCheck[$className])):
889 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
890 3
                    break;
891
892
                default:
893 4
                    $documentsToProcess = array();
894
895
            }
896
897 603
            foreach ($documentsToProcess as $document) {
898
                // Ignore uninitialized proxy objects
899 599
                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...
900 10
                    continue;
901
                }
902
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
903 599
                $oid = spl_object_hash($document);
904 599 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...
905 599
                    && ! isset($this->documentUpserts[$oid])
906 599
                    && ! isset($this->documentDeletions[$oid])
907 599
                    && isset($this->documentStates[$oid])
908
                ) {
909 603
                    $this->computeChangeSet($class, $document);
910
                }
911
            }
912
        }
913 603
    }
914
915
    /**
916
     * Computes the changes of an association.
917
     *
918
     * @param object $parentDocument
919
     * @param array $assoc
920
     * @param mixed $value The value of the association.
921
     * @throws \InvalidArgumentException
922
     */
923 448
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
924
    {
925 448
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
926 448
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
927 448
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
928
929 448
        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...
930 8
            return;
931
        }
932
933 447
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
934 246
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
935 242
                $this->scheduleCollectionUpdate($value);
936
            }
937 246
            $topmostOwner = $this->getOwningDocument($value->getOwner());
938 246
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
939 246
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
940 145
                $value->initialize();
941 145
                foreach ($value->getDeletedDocuments() as $orphan) {
942 23
                    $this->scheduleOrphanRemoval($orphan);
943
                }
944
            }
945
        }
946
947
        // Look through the documents, and in any of their associations,
948
        // for transient (new) documents, recursively. ("Persistence by reachability")
949
        // Unwrap. Uninitialized collections will simply be empty.
950 447
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
951
952 447
        $count = 0;
953 447
        foreach ($unwrappedValue as $key => $entry) {
954 352
            if ( ! is_object($entry)) {
955 1
                throw new \InvalidArgumentException(
956 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
957
                );
958
            }
959
960 351
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
961
962 351
            $state = $this->getDocumentState($entry, self::STATE_NEW);
963
964
            // Handle "set" strategy for multi-level hierarchy
965 351
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
966 351
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
967
968 351
            $count++;
969
970
            switch ($state) {
971 351
                case self::STATE_NEW:
972 63
                    if ( ! $assoc['isCascadePersist']) {
973
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
974
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
975
                            . ' Explicitly persist the new document or configure cascading persist operations'
976
                            . ' on the relationship.');
977
                    }
978
979 63
                    $this->persistNew($targetClass, $entry);
980 63
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
981 63
                    $this->computeChangeSet($targetClass, $entry);
982 63
                    break;
983
984 346
                case self::STATE_MANAGED:
985 346
                    if ($targetClass->isEmbeddedDocument) {
986 175
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
987 175
                        if ($knownParent && $knownParent !== $parentDocument) {
988 7
                            $entry = clone $entry;
989 7
                            if ($assoc['type'] === ClassMetadata::ONE) {
990 4
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
991 4
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
992
                            } else {
993
                                // must use unwrapped value to not trigger orphan removal
994 6
                                $unwrappedValue[$key] = $entry;
995
                            }
996 7
                            $this->persistNew($targetClass, $entry);
997
                        }
998 175
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
999 175
                        $this->computeChangeSet($targetClass, $entry);
1000
                    }
1001 346
                    break;
1002
1003 1
                case self::STATE_REMOVED:
1004
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1005
                    // and remove the element from Collection.
1006 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1007
                        unset($value[$key]);
1008
                    }
1009 1
                    break;
1010
1011
                case self::STATE_DETACHED:
1012
                    // Can actually not happen right now as we assume STATE_NEW,
1013
                    // so the exception will be raised from the DBAL layer (constraint violation).
1014
                    throw new \InvalidArgumentException('A detached document was found through a '
1015
                        . 'relationship during cascading a persist operation.');
1016
1017 351
                default:
1018
                    // MANAGED associated documents are already taken into account
1019
                    // during changeset calculation anyway, since they are in the identity map.
1020
1021
            }
1022
        }
1023 446
    }
1024
1025
    /**
1026
     * INTERNAL:
1027
     * Computes the changeset of an individual document, independently of the
1028
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1029
     *
1030
     * The passed document must be a managed document. If the document already has a change set
1031
     * because this method is invoked during a commit cycle then the change sets are added.
1032
     * whereby changes detected in this method prevail.
1033
     *
1034
     * @ignore
1035
     * @param ClassMetadata $class The class descriptor of the document.
1036
     * @param object $document The document for which to (re)calculate the change set.
1037
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1038
     */
1039 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1040
    {
1041
        // Ignore uninitialized proxy objects
1042 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...
1043 1
            return;
1044
        }
1045
1046 19
        $oid = spl_object_hash($document);
1047
1048 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1049
            throw new \InvalidArgumentException('Document must be managed.');
1050
        }
1051
1052 19
        if ( ! $class->isInheritanceTypeNone()) {
1053 2
            $class = $this->dm->getClassMetadata(get_class($document));
1054
        }
1055
1056 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1057 19
    }
1058
1059
    /**
1060
     * @param ClassMetadata $class
1061
     * @param object $document
1062
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1063
     */
1064 634
    private function persistNew(ClassMetadata $class, $document)
1065
    {
1066 634
        $this->lifecycleEventManager->prePersist($class, $document);
1067 634
        $oid = spl_object_hash($document);
1068 634
        $upsert = false;
1069 634
        if ($class->identifier) {
1070 634
            $idValue = $class->getIdentifierValue($document);
1071 634
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1072
1073 634
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1074 3
                throw new \InvalidArgumentException(sprintf(
1075 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1076
                    get_class($document)
1077
                ));
1078
            }
1079
1080 633
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1081 1
                throw new \InvalidArgumentException(sprintf(
1082 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1083
                    get_class($document)
1084
                ));
1085
            }
1086
1087 632
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1088 554
                $idValue = $class->idGenerator->generate($this->dm, $document);
1089 554
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1090 554
                $class->setIdentifierValue($document, $idValue);
1091
            }
1092
1093 632
            $this->documentIdentifiers[$oid] = $idValue;
1094
        } else {
1095
            // this is for embedded documents without identifiers
1096 158
            $this->documentIdentifiers[$oid] = $oid;
1097
        }
1098
1099 632
        $this->documentStates[$oid] = self::STATE_MANAGED;
1100
1101 632
        if ($upsert) {
1102 88
            $this->scheduleForUpsert($class, $document);
1103
        } else {
1104 561
            $this->scheduleForInsert($class, $document);
1105
        }
1106 632
    }
1107
1108
    /**
1109
     * Executes all document insertions for documents of the specified type.
1110
     *
1111
     * @param ClassMetadata $class
1112
     * @param array $documents Array of documents to insert
1113
     * @param array $options Array of options to be used with batchInsert()
1114
     */
1115 528 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...
1116
    {
1117 528
        $persister = $this->getDocumentPersister($class->name);
1118
1119 528
        foreach ($documents as $oid => $document) {
1120 528
            $persister->addInsert($document);
1121 528
            unset($this->documentInsertions[$oid]);
1122
        }
1123
1124 528
        $persister->executeInserts($options);
1125
1126 527
        foreach ($documents as $document) {
1127 527
            $this->lifecycleEventManager->postPersist($class, $document);
1128
        }
1129 527
    }
1130
1131
    /**
1132
     * Executes all document upserts for documents of the specified type.
1133
     *
1134
     * @param ClassMetadata $class
1135
     * @param array $documents Array of documents to upsert
1136
     * @param array $options Array of options to be used with batchInsert()
1137
     */
1138 85 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...
1139
    {
1140 85
        $persister = $this->getDocumentPersister($class->name);
1141
1142
1143 85
        foreach ($documents as $oid => $document) {
1144 85
            $persister->addUpsert($document);
1145 85
            unset($this->documentUpserts[$oid]);
1146
        }
1147
1148 85
        $persister->executeUpserts($options);
1149
1150 85
        foreach ($documents as $document) {
1151 85
            $this->lifecycleEventManager->postPersist($class, $document);
1152
        }
1153 85
    }
1154
1155
    /**
1156
     * Executes all document updates for documents of the specified type.
1157
     *
1158
     * @param Mapping\ClassMetadata $class
1159
     * @param array $documents Array of documents to update
1160
     * @param array $options Array of options to be used with update()
1161
     */
1162 232
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1163
    {
1164 232
        $className = $class->name;
1165 232
        $persister = $this->getDocumentPersister($className);
1166
1167 232
        foreach ($documents as $oid => $document) {
1168 232
            $this->lifecycleEventManager->preUpdate($class, $document);
1169
1170 232
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1171 230
                $persister->update($document, $options);
1172
            }
1173
1174 226
            unset($this->documentUpdates[$oid]);
1175
1176 226
            $this->lifecycleEventManager->postUpdate($class, $document);
1177
        }
1178 225
    }
1179
1180
    /**
1181
     * Executes all document deletions for documents of the specified type.
1182
     *
1183
     * @param ClassMetadata $class
1184
     * @param array $documents Array of documents to delete
1185
     * @param array $options Array of options to be used with remove()
1186
     */
1187 70
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1188
    {
1189 70
        $persister = $this->getDocumentPersister($class->name);
1190
1191 70
        foreach ($documents as $oid => $document) {
1192 70
            if ( ! $class->isEmbeddedDocument) {
1193 33
                $persister->delete($document, $options);
1194
            }
1195
            unset(
1196 68
                $this->documentDeletions[$oid],
1197 68
                $this->documentIdentifiers[$oid],
1198 68
                $this->originalDocumentData[$oid]
1199
            );
1200
1201
            // Clear snapshot information for any referenced PersistentCollection
1202
            // http://www.doctrine-project.org/jira/browse/MODM-95
1203 68
            foreach ($class->associationMappings as $fieldMapping) {
1204 44
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1205 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1206 27
                    if ($value instanceof PersistentCollectionInterface) {
1207 44
                        $value->clearSnapshot();
1208
                    }
1209
                }
1210
            }
1211
1212
            // Document with this $oid after deletion treated as NEW, even if the $oid
1213
            // is obtained by a new document because the old one went out of scope.
1214 68
            $this->documentStates[$oid] = self::STATE_NEW;
1215
1216 68
            $this->lifecycleEventManager->postRemove($class, $document);
1217
        }
1218 68
    }
1219
1220
    /**
1221
     * Schedules a document for insertion into the database.
1222
     * If the document already has an identifier, it will be added to the
1223
     * identity map.
1224
     *
1225
     * @param ClassMetadata $class
1226
     * @param object $document The document to schedule for insertion.
1227
     * @throws \InvalidArgumentException
1228
     */
1229 564
    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...
1230
    {
1231 564
        $oid = spl_object_hash($document);
1232
1233 564
        if (isset($this->documentUpdates[$oid])) {
1234
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1235
        }
1236 564
        if (isset($this->documentDeletions[$oid])) {
1237
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1238
        }
1239 564
        if (isset($this->documentInsertions[$oid])) {
1240
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1241
        }
1242
1243 564
        $this->documentInsertions[$oid] = $document;
1244
1245 564
        if (isset($this->documentIdentifiers[$oid])) {
1246 561
            $this->addToIdentityMap($document);
1247
        }
1248 564
    }
1249
1250
    /**
1251
     * Schedules a document for upsert into the database and adds it to the
1252
     * identity map
1253
     *
1254
     * @param ClassMetadata $class
1255
     * @param object $document The document to schedule for upsert.
1256
     * @throws \InvalidArgumentException
1257
     */
1258 91
    public function scheduleForUpsert(ClassMetadata $class, $document)
1259
    {
1260 91
        $oid = spl_object_hash($document);
1261
1262 91
        if ($class->isEmbeddedDocument) {
1263
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1264
        }
1265 91
        if (isset($this->documentUpdates[$oid])) {
1266
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1267
        }
1268 91
        if (isset($this->documentDeletions[$oid])) {
1269
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1270
        }
1271 91
        if (isset($this->documentUpserts[$oid])) {
1272
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1273
        }
1274
1275 91
        $this->documentUpserts[$oid] = $document;
1276 91
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1277 91
        $this->addToIdentityMap($document);
1278 91
    }
1279
1280
    /**
1281
     * Checks whether a document is scheduled for insertion.
1282
     *
1283
     * @param object $document
1284
     * @return boolean
1285
     */
1286 105
    public function isScheduledForInsert($document)
1287
    {
1288 105
        return isset($this->documentInsertions[spl_object_hash($document)]);
1289
    }
1290
1291
    /**
1292
     * Checks whether a document is scheduled for upsert.
1293
     *
1294
     * @param object $document
1295
     * @return boolean
1296
     */
1297 5
    public function isScheduledForUpsert($document)
1298
    {
1299 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1300
    }
1301
1302
    /**
1303
     * Schedules a document for being updated.
1304
     *
1305
     * @param object $document The document to schedule for being updated.
1306
     * @throws \InvalidArgumentException
1307
     */
1308 241
    public function scheduleForUpdate($document)
1309
    {
1310 241
        $oid = spl_object_hash($document);
1311 241
        if ( ! isset($this->documentIdentifiers[$oid])) {
1312
            throw new \InvalidArgumentException('Document has no identity.');
1313
        }
1314
1315 241
        if (isset($this->documentDeletions[$oid])) {
1316
            throw new \InvalidArgumentException('Document is removed.');
1317
        }
1318
1319 241
        if ( ! isset($this->documentUpdates[$oid])
1320 241
            && ! isset($this->documentInsertions[$oid])
1321 241
            && ! isset($this->documentUpserts[$oid])) {
1322 237
            $this->documentUpdates[$oid] = $document;
1323
        }
1324 241
    }
1325
1326
    /**
1327
     * Checks whether a document is registered as dirty in the unit of work.
1328
     * Note: Is not very useful currently as dirty documents are only registered
1329
     * at commit time.
1330
     *
1331
     * @param object $document
1332
     * @return boolean
1333
     */
1334 22
    public function isScheduledForUpdate($document)
1335
    {
1336 22
        return isset($this->documentUpdates[spl_object_hash($document)]);
1337
    }
1338
1339 1
    public function isScheduledForDirtyCheck($document)
1340
    {
1341 1
        $class = $this->dm->getClassMetadata(get_class($document));
1342 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1343
    }
1344
1345
    /**
1346
     * INTERNAL:
1347
     * Schedules a document for deletion.
1348
     *
1349
     * @param object $document
1350
     */
1351 75
    public function scheduleForDelete($document)
1352
    {
1353 75
        $oid = spl_object_hash($document);
1354
1355 75
        if (isset($this->documentInsertions[$oid])) {
1356 2
            if ($this->isInIdentityMap($document)) {
1357 2
                $this->removeFromIdentityMap($document);
1358
            }
1359 2
            unset($this->documentInsertions[$oid]);
1360 2
            return; // document has not been persisted yet, so nothing more to do.
1361
        }
1362
1363 74
        if ( ! $this->isInIdentityMap($document)) {
1364 2
            return; // ignore
1365
        }
1366
1367 73
        $this->removeFromIdentityMap($document);
1368 73
        $this->documentStates[$oid] = self::STATE_REMOVED;
1369
1370 73
        if (isset($this->documentUpdates[$oid])) {
1371
            unset($this->documentUpdates[$oid]);
1372
        }
1373 73
        if ( ! isset($this->documentDeletions[$oid])) {
1374 73
            $this->documentDeletions[$oid] = $document;
1375
        }
1376 73
    }
1377
1378
    /**
1379
     * Checks whether a document is registered as removed/deleted with the unit
1380
     * of work.
1381
     *
1382
     * @param object $document
1383
     * @return boolean
1384
     */
1385 8
    public function isScheduledForDelete($document)
1386
    {
1387 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1388
    }
1389
1390
    /**
1391
     * Checks whether a document is scheduled for insertion, update or deletion.
1392
     *
1393
     * @param $document
1394
     * @return boolean
1395
     */
1396 245
    public function isDocumentScheduled($document)
1397
    {
1398 245
        $oid = spl_object_hash($document);
1399 245
        return isset($this->documentInsertions[$oid]) ||
1400 130
            isset($this->documentUpserts[$oid]) ||
1401 120
            isset($this->documentUpdates[$oid]) ||
1402 245
            isset($this->documentDeletions[$oid]);
1403
    }
1404
1405
    /**
1406
     * INTERNAL:
1407
     * Registers a document in the identity map.
1408
     *
1409
     * Note that documents in a hierarchy are registered with the class name of
1410
     * the root document. Identifiers are serialized before being used as array
1411
     * keys to allow differentiation of equal, but not identical, values.
1412
     *
1413
     * @ignore
1414
     * @param object $document  The document to register.
1415
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1416
     *                  the document in question is already managed.
1417
     */
1418 663
    public function addToIdentityMap($document)
1419
    {
1420 663
        $class = $this->dm->getClassMetadata(get_class($document));
1421 663
        $id = $this->getIdForIdentityMap($document);
1422
1423 663
        if (isset($this->identityMap[$class->name][$id])) {
1424 55
            return false;
1425
        }
1426
1427 663
        $this->identityMap[$class->name][$id] = $document;
1428
1429 663
        if ($document instanceof NotifyPropertyChanged &&
1430 663
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1431 4
            $document->addPropertyChangedListener($this);
1432
        }
1433
1434 663
        return true;
1435
    }
1436
1437
    /**
1438
     * Gets the state of a document with regard to the current unit of work.
1439
     *
1440
     * @param object   $document
1441
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1442
     *                         This parameter can be set to improve performance of document state detection
1443
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1444
     *                         is either known or does not matter for the caller of the method.
1445
     * @return int The document state.
1446
     */
1447 637
    public function getDocumentState($document, $assume = null)
1448
    {
1449 637
        $oid = spl_object_hash($document);
1450
1451 637
        if (isset($this->documentStates[$oid])) {
1452 394
            return $this->documentStates[$oid];
1453
        }
1454
1455 637
        $class = $this->dm->getClassMetadata(get_class($document));
1456
1457 637
        if ($class->isEmbeddedDocument) {
1458 193
            return self::STATE_NEW;
1459
        }
1460
1461 634
        if ($assume !== null) {
1462 631
            return $assume;
1463
        }
1464
1465
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1466
         * known. Note that you cannot remember the NEW or DETACHED state in
1467
         * _documentStates since the UoW does not hold references to such
1468
         * objects and the object hash can be reused. More generally, because
1469
         * the state may "change" between NEW/DETACHED without the UoW being
1470
         * aware of it.
1471
         */
1472 4
        $id = $class->getIdentifierObject($document);
1473
1474 4
        if ($id === null) {
1475 3
            return self::STATE_NEW;
1476
        }
1477
1478
        // Check for a version field, if available, to avoid a DB lookup.
1479 2
        if ($class->isVersioned) {
1480
            return $class->getFieldValue($document, $class->versionField)
1481
                ? self::STATE_DETACHED
1482
                : self::STATE_NEW;
1483
        }
1484
1485
        // Last try before DB lookup: check the identity map.
1486 2
        if ($this->tryGetById($id, $class)) {
1487 1
            return self::STATE_DETACHED;
1488
        }
1489
1490
        // DB lookup
1491 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1492 1
            return self::STATE_DETACHED;
1493
        }
1494
1495 1
        return self::STATE_NEW;
1496
    }
1497
1498
    /**
1499
     * INTERNAL:
1500
     * Removes a document from the identity map. This effectively detaches the
1501
     * document from the persistence management of Doctrine.
1502
     *
1503
     * @ignore
1504
     * @param object $document
1505
     * @throws \InvalidArgumentException
1506
     * @return boolean
1507
     */
1508 88
    public function removeFromIdentityMap($document)
1509
    {
1510 88
        $oid = spl_object_hash($document);
1511
1512
        // Check if id is registered first
1513 88
        if ( ! isset($this->documentIdentifiers[$oid])) {
1514
            return false;
1515
        }
1516
1517 88
        $class = $this->dm->getClassMetadata(get_class($document));
1518 88
        $id = $this->getIdForIdentityMap($document);
1519
1520 88
        if (isset($this->identityMap[$class->name][$id])) {
1521 88
            unset($this->identityMap[$class->name][$id]);
1522 88
            $this->documentStates[$oid] = self::STATE_DETACHED;
1523 88
            return true;
1524
        }
1525
1526
        return false;
1527
    }
1528
1529
    /**
1530
     * INTERNAL:
1531
     * Gets a document in the identity map by its identifier hash.
1532
     *
1533
     * @ignore
1534
     * @param mixed         $id    Document identifier
1535
     * @param ClassMetadata $class Document class
1536
     * @return object
1537
     * @throws InvalidArgumentException if the class does not have an identifier
1538
     */
1539 34
    public function getById($id, ClassMetadata $class)
1540
    {
1541 34
        if ( ! $class->identifier) {
1542
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1543
        }
1544
1545 34
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1546
1547 34
        return $this->identityMap[$class->name][$serializedId];
1548
    }
1549
1550
    /**
1551
     * INTERNAL:
1552
     * Tries to get a document by its identifier hash. If no document is found
1553
     * for the given hash, FALSE is returned.
1554
     *
1555
     * @ignore
1556
     * @param mixed         $id    Document identifier
1557
     * @param ClassMetadata $class Document class
1558
     * @return mixed The found document or FALSE.
1559
     * @throws InvalidArgumentException if the class does not have an identifier
1560
     */
1561 305
    public function tryGetById($id, ClassMetadata $class)
1562
    {
1563 305
        if ( ! $class->identifier) {
1564
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1565
        }
1566
1567 305
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1568
1569 305
        return isset($this->identityMap[$class->name][$serializedId]) ?
1570 305
            $this->identityMap[$class->name][$serializedId] : false;
1571
    }
1572
1573
    /**
1574
     * Schedules a document for dirty-checking at commit-time.
1575
     *
1576
     * @param object $document The document to schedule for dirty-checking.
1577
     * @todo Rename: scheduleForSynchronization
1578
     */
1579 3
    public function scheduleForDirtyCheck($document)
1580
    {
1581 3
        $class = $this->dm->getClassMetadata(get_class($document));
1582 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1583 3
    }
1584
1585
    /**
1586
     * Checks whether a document is registered in the identity map.
1587
     *
1588
     * @param object $document
1589
     * @return boolean
1590
     */
1591 86
    public function isInIdentityMap($document)
1592
    {
1593 86
        $oid = spl_object_hash($document);
1594
1595 86
        if ( ! isset($this->documentIdentifiers[$oid])) {
1596 6
            return false;
1597
        }
1598
1599 84
        $class = $this->dm->getClassMetadata(get_class($document));
1600 84
        $id = $this->getIdForIdentityMap($document);
1601
1602 84
        return isset($this->identityMap[$class->name][$id]);
1603
    }
1604
1605
    /**
1606
     * @param object $document
1607
     * @return string
1608
     */
1609 663
    private function getIdForIdentityMap($document)
1610
    {
1611 663
        $class = $this->dm->getClassMetadata(get_class($document));
1612
1613 663
        if ( ! $class->identifier) {
1614 161
            $id = spl_object_hash($document);
1615
        } else {
1616 662
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1617 662
            $id = serialize($class->getDatabaseIdentifierValue($id));
1618
        }
1619
1620 663
        return $id;
1621
    }
1622
1623
    /**
1624
     * INTERNAL:
1625
     * Checks whether an identifier exists in the identity map.
1626
     *
1627
     * @ignore
1628
     * @param string $id
1629
     * @param string $rootClassName
1630
     * @return boolean
1631
     */
1632
    public function containsId($id, $rootClassName)
1633
    {
1634
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1635
    }
1636
1637
    /**
1638
     * Persists a document as part of the current unit of work.
1639
     *
1640
     * @param object $document The document to persist.
1641
     * @throws MongoDBException If trying to persist MappedSuperclass.
1642
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1643
     */
1644 631
    public function persist($document)
1645
    {
1646 631
        $class = $this->dm->getClassMetadata(get_class($document));
1647 631
        if ($class->isMappedSuperclass) {
1648 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1649
        }
1650 630
        $visited = array();
1651 630
        $this->doPersist($document, $visited);
1652 626
    }
1653
1654
    /**
1655
     * Saves a document as part of the current unit of work.
1656
     * This method is internally called during save() cascades as it tracks
1657
     * the already visited documents to prevent infinite recursions.
1658
     *
1659
     * NOTE: This method always considers documents that are not yet known to
1660
     * this UnitOfWork as NEW.
1661
     *
1662
     * @param object $document The document to persist.
1663
     * @param array $visited The already visited documents.
1664
     * @throws \InvalidArgumentException
1665
     * @throws MongoDBException
1666
     */
1667 630
    private function doPersist($document, array &$visited)
1668
    {
1669 630
        $oid = spl_object_hash($document);
1670 630
        if (isset($visited[$oid])) {
1671 24
            return; // Prevent infinite recursion
1672
        }
1673
1674 630
        $visited[$oid] = $document; // Mark visited
1675
1676 630
        $class = $this->dm->getClassMetadata(get_class($document));
1677
1678 630
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1679
        switch ($documentState) {
1680 630
            case self::STATE_MANAGED:
1681
                // Nothing to do, except if policy is "deferred explicit"
1682 51
                if ($class->isChangeTrackingDeferredExplicit()) {
1683
                    $this->scheduleForDirtyCheck($document);
1684
                }
1685 51
                break;
1686 630
            case self::STATE_NEW:
1687 630
                $this->persistNew($class, $document);
1688 628
                break;
1689
1690 2
            case self::STATE_REMOVED:
1691
                // Document becomes managed again
1692 2
                unset($this->documentDeletions[$oid]);
1693
1694 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1695 2
                break;
1696
1697
            case self::STATE_DETACHED:
1698
                throw new \InvalidArgumentException(
1699
                    'Behavior of persist() for a detached document is not yet defined.');
1700
1701
            default:
1702
                throw MongoDBException::invalidDocumentState($documentState);
1703
        }
1704
1705 628
        $this->cascadePersist($document, $visited);
1706 626
    }
1707
1708
    /**
1709
     * Deletes a document as part of the current unit of work.
1710
     *
1711
     * @param object $document The document to remove.
1712
     */
1713 74
    public function remove($document)
1714
    {
1715 74
        $visited = array();
1716 74
        $this->doRemove($document, $visited);
1717 74
    }
1718
1719
    /**
1720
     * Deletes a document as part of the current unit of work.
1721
     *
1722
     * This method is internally called during delete() cascades as it tracks
1723
     * the already visited documents to prevent infinite recursions.
1724
     *
1725
     * @param object $document The document to delete.
1726
     * @param array $visited The map of the already visited documents.
1727
     * @throws MongoDBException
1728
     */
1729 74
    private function doRemove($document, array &$visited)
1730
    {
1731 74
        $oid = spl_object_hash($document);
1732 74
        if (isset($visited[$oid])) {
1733 1
            return; // Prevent infinite recursion
1734
        }
1735
1736 74
        $visited[$oid] = $document; // mark visited
1737
1738
        /* Cascade first, because scheduleForDelete() removes the entity from
1739
         * the identity map, which can cause problems when a lazy Proxy has to
1740
         * be initialized for the cascade operation.
1741
         */
1742 74
        $this->cascadeRemove($document, $visited);
1743
1744 74
        $class = $this->dm->getClassMetadata(get_class($document));
1745 74
        $documentState = $this->getDocumentState($document);
1746
        switch ($documentState) {
1747 74
            case self::STATE_NEW:
1748 74
            case self::STATE_REMOVED:
1749
                // nothing to do
1750 1
                break;
1751 74
            case self::STATE_MANAGED:
1752 74
                $this->lifecycleEventManager->preRemove($class, $document);
1753 74
                $this->scheduleForDelete($document);
1754 74
                break;
1755
            case self::STATE_DETACHED:
1756
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1757
            default:
1758
                throw MongoDBException::invalidDocumentState($documentState);
1759
        }
1760 74
    }
1761
1762
    /**
1763
     * Merges the state of the given detached document into this UnitOfWork.
1764
     *
1765
     * @param object $document
1766
     * @return object The managed copy of the document.
1767
     */
1768 15
    public function merge($document)
1769
    {
1770 15
        $visited = array();
1771
1772 15
        return $this->doMerge($document, $visited);
1773
    }
1774
1775
    /**
1776
     * Executes a merge operation on a document.
1777
     *
1778
     * @param object      $document
1779
     * @param array       $visited
1780
     * @param object|null $prevManagedCopy
1781
     * @param array|null  $assoc
1782
     *
1783
     * @return object The managed copy of the document.
1784
     *
1785
     * @throws InvalidArgumentException If the entity instance is NEW.
1786
     * @throws LockException If the document uses optimistic locking through a
1787
     *                       version attribute and the version check against the
1788
     *                       managed copy fails.
1789
     */
1790 15
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1791
    {
1792 15
        $oid = spl_object_hash($document);
1793
1794 15
        if (isset($visited[$oid])) {
1795 1
            return $visited[$oid]; // Prevent infinite recursion
1796
        }
1797
1798 15
        $visited[$oid] = $document; // mark visited
1799
1800 15
        $class = $this->dm->getClassMetadata(get_class($document));
1801
1802
        /* First we assume DETACHED, although it can still be NEW but we can
1803
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1804
         * an identity, we need to fetch it from the DB anyway in order to
1805
         * merge. MANAGED documents are ignored by the merge operation.
1806
         */
1807 15
        $managedCopy = $document;
1808
1809 15
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1810 15
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1811
                $document->__load();
1812
            }
1813
1814 15
            $identifier = $class->getIdentifier();
1815
            // We always have one element in the identifier array but it might be null
1816 15
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1817 15
            $managedCopy = null;
1818
1819
            // Try to fetch document from the database
1820 15
            if (! $class->isEmbeddedDocument && $id !== null) {
1821 12
                $managedCopy = $this->dm->find($class->name, $id);
1822
1823
                // Managed copy may be removed in which case we can't merge
1824 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1825
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1826
                }
1827
1828 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...
1829
                    $managedCopy->__load();
1830
                }
1831
            }
1832
1833 15
            if ($managedCopy === null) {
1834
                // Create a new managed instance
1835 7
                $managedCopy = $class->newInstance();
1836 7
                if ($id !== null) {
1837 3
                    $class->setIdentifierValue($managedCopy, $id);
1838
                }
1839 7
                $this->persistNew($class, $managedCopy);
1840
            }
1841
1842 15
            if ($class->isVersioned) {
1843
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1844
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1845
1846
                // Throw exception if versions don't match
1847
                if ($managedCopyVersion != $documentVersion) {
1848
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1849
                }
1850
            }
1851
1852
            // Merge state of $document into existing (managed) document
1853 15
            foreach ($class->reflClass->getProperties() as $prop) {
1854 15
                $name = $prop->name;
1855 15
                $prop->setAccessible(true);
1856 15
                if ( ! isset($class->associationMappings[$name])) {
1857 15
                    if ( ! $class->isIdentifier($name)) {
1858 15
                        $prop->setValue($managedCopy, $prop->getValue($document));
1859
                    }
1860
                } else {
1861 15
                    $assoc2 = $class->associationMappings[$name];
1862
1863 15
                    if ($assoc2['type'] === 'one') {
1864 7
                        $other = $prop->getValue($document);
1865
1866 7
                        if ($other === null) {
1867 2
                            $prop->setValue($managedCopy, null);
1868 6
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1869
                            // Do not merge fields marked lazy that have not been fetched
1870 1
                            continue;
1871 5
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1872
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1873
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1874
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1875
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1876
                                $relatedId = $targetClass->getIdentifierObject($other);
1877
1878
                                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...
1879
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1880
                                } else {
1881
                                    $other = $this
1882
                                        ->dm
1883
                                        ->getProxyFactory()
1884
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1885
                                    $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...
1886
                                }
1887
                            }
1888
1889 6
                            $prop->setValue($managedCopy, $other);
1890
                        }
1891
                    } else {
1892 12
                        $mergeCol = $prop->getValue($document);
1893
1894 12
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1895
                            /* Do not merge fields marked lazy that have not
1896
                             * been fetched. Keep the lazy persistent collection
1897
                             * of the managed copy.
1898
                             */
1899 3
                            continue;
1900
                        }
1901
1902 12
                        $managedCol = $prop->getValue($managedCopy);
1903
1904 12
                        if ( ! $managedCol) {
1905 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1906 3
                            $managedCol->setOwner($managedCopy, $assoc2);
1907 3
                            $prop->setValue($managedCopy, $managedCol);
1908 3
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1909
                        }
1910
1911
                        /* Note: do not process association's target documents.
1912
                         * They will be handled during the cascade. Initialize
1913
                         * and, if necessary, clear $managedCol for now.
1914
                         */
1915 12
                        if ($assoc2['isCascadeMerge']) {
1916 12
                            $managedCol->initialize();
1917
1918
                            // If $managedCol differs from the merged collection, clear and set dirty
1919 12
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1920 3
                                $managedCol->unwrap()->clear();
1921 3
                                $managedCol->setDirty(true);
1922
1923 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1924
                                    $this->scheduleForDirtyCheck($managedCopy);
1925
                                }
1926
                            }
1927
                        }
1928
                    }
1929
                }
1930
1931 15
                if ($class->isChangeTrackingNotify()) {
1932
                    // Just treat all properties as changed, there is no other choice.
1933 15
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1934
                }
1935
            }
1936
1937 15
            if ($class->isChangeTrackingDeferredExplicit()) {
1938
                $this->scheduleForDirtyCheck($document);
1939
            }
1940
        }
1941
1942 15
        if ($prevManagedCopy !== null) {
1943 8
            $assocField = $assoc['fieldName'];
1944 8
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1945
1946 8
            if ($assoc['type'] === 'one') {
1947 4
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1948
            } else {
1949 6
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1950
1951 6
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1952 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1953
                }
1954
            }
1955
        }
1956
1957
        // Mark the managed copy visited as well
1958 15
        $visited[spl_object_hash($managedCopy)] = true;
1959
1960 15
        $this->cascadeMerge($document, $managedCopy, $visited);
1961
1962 15
        return $managedCopy;
1963
    }
1964
1965
    /**
1966
     * Detaches a document from the persistence management. It's persistence will
1967
     * no longer be managed by Doctrine.
1968
     *
1969
     * @param object $document The document to detach.
1970
     */
1971 11
    public function detach($document)
1972
    {
1973 11
        $visited = array();
1974 11
        $this->doDetach($document, $visited);
1975 11
    }
1976
1977
    /**
1978
     * Executes a detach operation on the given document.
1979
     *
1980
     * @param object $document
1981
     * @param array $visited
1982
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1983
     */
1984 16
    private function doDetach($document, array &$visited)
1985
    {
1986 16
        $oid = spl_object_hash($document);
1987 16
        if (isset($visited[$oid])) {
1988 4
            return; // Prevent infinite recursion
1989
        }
1990
1991 16
        $visited[$oid] = $document; // mark visited
1992
1993 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1994 16
            case self::STATE_MANAGED:
1995 16
                $this->removeFromIdentityMap($document);
1996 16
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1997 16
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1998 16
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1999 16
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2000 16
                    $this->hasScheduledCollections[$oid], $this->embeddedDocumentsRegistry[$oid]);
2001 16
                break;
2002 4
            case self::STATE_NEW:
2003 4
            case self::STATE_DETACHED:
2004 4
                return;
2005
        }
2006
2007 16
        $this->cascadeDetach($document, $visited);
2008 16
    }
2009
2010
    /**
2011
     * Refreshes the state of the given document from the database, overwriting
2012
     * any local, unpersisted changes.
2013
     *
2014
     * @param object $document The document to refresh.
2015
     * @throws \InvalidArgumentException If the document is not MANAGED.
2016
     */
2017 22
    public function refresh($document)
2018
    {
2019 22
        $visited = array();
2020 22
        $this->doRefresh($document, $visited);
2021 21
    }
2022
2023
    /**
2024
     * Executes a refresh operation on a document.
2025
     *
2026
     * @param object $document The document to refresh.
2027
     * @param array $visited The already visited documents during cascades.
2028
     * @throws \InvalidArgumentException If the document is not MANAGED.
2029
     */
2030 22
    private function doRefresh($document, array &$visited)
2031
    {
2032 22
        $oid = spl_object_hash($document);
2033 22
        if (isset($visited[$oid])) {
2034
            return; // Prevent infinite recursion
2035
        }
2036
2037 22
        $visited[$oid] = $document; // mark visited
2038
2039 22
        $class = $this->dm->getClassMetadata(get_class($document));
2040
2041 22
        if ( ! $class->isEmbeddedDocument) {
2042 22
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2043 21
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2044 21
                $this->getDocumentPersister($class->name)->refresh($id, $document);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Per...entPersister::refresh() has been deprecated with message: The first argument is deprecated.

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

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

Loading history...
2045
            } else {
2046 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2047
            }
2048
        }
2049
2050 21
        $this->cascadeRefresh($document, $visited);
2051 21
    }
2052
2053
    /**
2054
     * Cascades a refresh operation to associated documents.
2055
     *
2056
     * @param object $document
2057
     * @param array $visited
2058
     */
2059 21
    private function cascadeRefresh($document, array &$visited)
2060
    {
2061 21
        $class = $this->dm->getClassMetadata(get_class($document));
2062
2063 21
        $associationMappings = array_filter(
2064 21
            $class->associationMappings,
2065
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2066
        );
2067
2068 21
        foreach ($associationMappings as $mapping) {
2069 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2070 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2071 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2072
                    // Unwrap so that foreach() does not initialize
2073 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2074
                }
2075 15
                foreach ($relatedDocuments as $relatedDocument) {
2076 15
                    $this->doRefresh($relatedDocument, $visited);
2077
                }
2078 10
            } elseif ($relatedDocuments !== null) {
2079 15
                $this->doRefresh($relatedDocuments, $visited);
2080
            }
2081
        }
2082 21
    }
2083
2084
    /**
2085
     * Cascades a detach operation to associated documents.
2086
     *
2087
     * @param object $document
2088
     * @param array $visited
2089
     */
2090 16 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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