Completed
Pull Request — master (#1331)
by Maciej
10:31
created

UnitOfWork::isScheduledForUpsert()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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

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

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

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

Loading history...
405 22
            $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 557
        ) {
407 22
            return; // Nothing to do.
408
        }
409
410 554
        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 33
            foreach ($this->orphanRemovals as $removal) {
412 33
                $this->remove($removal);
413 33
            }
414 33
        }
415
416
        // Raise onFlush
417 554
        if ($this->evm->hasListeners(Events::onFlush)) {
418 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
419 7
        }
420
421 554
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
422 78
            list($class, $documents) = $classAndDocuments;
423 78
            $this->executeUpserts($class, $documents, $options);
424 554
        }
425
426 554
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
427 487
            list($class, $documents) = $classAndDocuments;
428 487
            $this->executeInserts($class, $documents, $options);
429 553
        }
430
431 553
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
432 218
            list($class, $documents) = $classAndDocuments;
433 218
            $this->executeUpdates($class, $documents, $options);
434 553
        }
435
436 553
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
437 12
            list($class, $documents) = $classAndDocuments;
438 12
            $this->executeDeletions($class, $documents, $options);
439 553
        }
440
441
        // Raise postFlush
442 553
        if ($this->evm->hasListeners(Events::postFlush)) {
443
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
444
        }
445
446
        // Clear up
447 553
        $this->documentInsertions =
448 553
        $this->documentUpserts =
449 553
        $this->documentUpdates =
450 553
        $this->documentDeletions =
451 553
        $this->documentChangeSets =
452 553
        $this->collectionUpdates =
453 553
        $this->collectionDeletions =
454 553
        $this->visitedCollections =
455 553
        $this->scheduledForDirtyCheck =
456 553
        $this->orphanRemovals = 
457 553
        $this->hasScheduledCollections = array();
458 553
    }
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 554
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
468
    {
469 554
        if (empty($documents)) {
470 554
            return array();
471
        }
472 553
        $divided = array();
473 553
        $embeds = array();
474 553
        foreach ($documents as $oid => $d) {
475 553
            $className = get_class($d);
476 553
            if (isset($embeds[$className])) {
477 67
                continue;
478
            }
479 553
            if (isset($divided[$className])) {
480 120
                $divided[$className][1][$oid] = $d;
481 120
                continue;
482
            }
483 553
            $class = $this->dm->getClassMetadata($className);
484 553
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
485 166
                $embeds[$className] = true;
486 166
                continue;
487
            }
488 553
            if (empty($divided[$class->name])) {
489 553
                $divided[$class->name] = array($class, array($oid => $d));
490 553
            } else {
491 3
                $divided[$class->name][1][$oid] = $d;
492
            }
493 553
        }
494 553
        return $divided;
495
    }
496
497
    /**
498
     * Compute changesets of all documents scheduled for insertion.
499
     *
500
     * Embedded documents will not be processed.
501
     */
502 561 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 561
        foreach ($this->documentInsertions as $document) {
505 495
            $class = $this->dm->getClassMetadata(get_class($document));
506 495
            if ( ! $class->isEmbeddedDocument) {
507 492
                $this->computeChangeSet($class, $document);
508 491
            }
509 560
        }
510 560
    }
511
512
    /**
513
     * Compute changesets of all documents scheduled for upsert.
514
     *
515
     * Embedded documents will not be processed.
516
     */
517 560 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 560
        foreach ($this->documentUpserts as $document) {
520 77
            $class = $this->dm->getClassMetadata(get_class($document));
521 77
            if ( ! $class->isEmbeddedDocument) {
522 77
                $this->computeChangeSet($class, $document);
523 77
            }
524 560
        }
525 560
    }
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 9
        }
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 12
        ) {
569 8
            $this->computeChangeSet($class, $document);
570 8
        }
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 551
    public function getDocumentChangeSet($document)
580
    {
581 551
        $oid = spl_object_hash($document);
582 551
        if (isset($this->documentChangeSets[$oid])) {
583 551
            return $this->documentChangeSets[$oid];
584
        }
585 51
        return array();
586
    }
587
588
    /**
589
     * Get a documents actual data, flattening all the objects to arrays.
590
     *
591
     * @param object $document
592
     * @return array
593
     */
594 558
    public function getDocumentActualData($document)
595
    {
596 558
        $class = $this->dm->getClassMetadata(get_class($document));
597 558
        $actualData = array();
598 558
        foreach ($class->reflFields as $name => $refProp) {
599 558
            $mapping = $class->fieldMappings[$name];
600
            // skip not saved fields
601 558
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
602 49
                continue;
603
            }
604 558
            $value = $refProp->getValue($document);
605 558
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
606 5
                $value = new GridFSFile($value);
607 5
                $class->reflFields[$name]->setValue($document, $value);
608 5
                $actualData[$name] = $value;
609 558
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
610 558
                && $value !== null && ! ($value instanceof PersistentCollection)) {
611
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
612 366
                if ( ! $value instanceof Collection) {
613 119
                    $value = new ArrayCollection($value);
614 119
                }
615
616
                // Inject PersistentCollection
617 366
                $coll = new PersistentCollection($value, $this->dm, $this);
618 366
                $coll->setOwner($document, $mapping);
619 366
                $coll->setDirty( ! $value->isEmpty());
620 366
                $class->reflFields[$name]->setValue($document, $coll);
621 366
                $actualData[$name] = $coll;
622 366
            } else {
623 558
                $actualData[$name] = $value;
624
            }
625 558
        }
626 558
        return $actualData;
627
    }
628
629
    /**
630
     * Computes the changes that happened to a single document.
631
     *
632
     * Modifies/populates the following properties:
633
     *
634
     * {@link originalDocumentData}
635
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
636
     * then it was not fetched from the database and therefore we have no original
637
     * document data yet. All of the current document data is stored as the original document data.
638
     *
639
     * {@link documentChangeSets}
640
     * The changes detected on all properties of the document are stored there.
641
     * A change is a tuple array where the first entry is the old value and the second
642
     * entry is the new value of the property. Changesets are used by persisters
643
     * to INSERT/UPDATE the persistent document state.
644
     *
645
     * {@link documentUpdates}
646
     * If the document is already fully MANAGED (has been fetched from the database before)
647
     * and any changes to its properties are detected, then a reference to the document is stored
648
     * there to mark it for an update.
649
     *
650
     * @param ClassMetadata $class The class descriptor of the document.
651
     * @param object $document The document for which to compute the changes.
652
     */
653 558
    public function computeChangeSet(ClassMetadata $class, $document)
654
    {
655 558
        if ( ! $class->isInheritanceTypeNone()) {
656 172
            $class = $this->dm->getClassMetadata(get_class($document));
657 172
        }
658
659
        // Fire PreFlush lifecycle callbacks
660 558 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...
661 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
662 11
        }
663
664 558
        $this->computeOrRecomputeChangeSet($class, $document);
665 557
    }
666
667
    /**
668
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
669
     *
670
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
671
     * @param object $document
672
     * @param boolean $recompute
673
     */
674 558
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
675
    {
676 558
        $oid = spl_object_hash($document);
677 558
        $actualData = $this->getDocumentActualData($document);
678 558
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
679 558
        if ($isNewDocument) {
680
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
681
            // These result in an INSERT.
682 558
            $this->originalDocumentData[$oid] = $actualData;
683 558
            $changeSet = array();
684 558
            foreach ($actualData as $propName => $actualValue) {
685
                /* At this PersistentCollection shouldn't be here, probably it
686
                 * was cloned and its ownership must be fixed
687
                 */
688 558
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
689
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
690
                    $actualValue = $actualData[$propName];
691
                }
692
                // ignore inverse side of reference relationship
693 558 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...
694 172
                    continue;
695
                }
696 558
                $changeSet[$propName] = array(null, $actualValue);
697 558
            }
698 558
            $this->documentChangeSets[$oid] = $changeSet;
699 558
        } else {
700
            // Document is "fully" MANAGED: it was already fully persisted before
701
            // and we have a copy of the original data
702 278
            $originalData = $this->originalDocumentData[$oid];
703 278
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
704 278
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
705 2
                $changeSet = $this->documentChangeSets[$oid];
706 2
            } else {
707 277
                $changeSet = array();
708
            }
709
710 278
            foreach ($actualData as $propName => $actualValue) {
711
                // skip not saved fields
712 278
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
713
                    continue;
714
                }
715
716 278
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
717
718
                // skip if value has not changed
719 278
                if ($orgValue === $actualValue) {
720
                    // but consider dirty GridFSFile instances as changed
721 277
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
722 277
                        continue;
723
                    }
724 1
                }
725
726
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
727 173
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
728 10
                    if ($orgValue !== null) {
729 4
                        $this->scheduleOrphanRemoval($orgValue);
730 4
                    }
731
732 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
733 10
                    continue;
734
                }
735
736
                // if owning side of reference-one relationship
737 163
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
738 10
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
739 1
                        $this->scheduleOrphanRemoval($orgValue);
740 1
                    }
741
742 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
743 10
                    continue;
744
                }
745
746 155
                if ($isChangeTrackingNotify) {
747 2
                    continue;
748
                }
749
750
                // ignore inverse side of reference relationship
751 154 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...
752 2
                    continue;
753
                }
754
755
                // Persistent collection was exchanged with the "originally"
756
                // created one. This can only mean it was cloned and replaced
757
                // on another document.
758 153
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
759 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
760 6
                }
761
762
                // if embed-many or reference-many relationship
763 153
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
764 22
                    $changeSet[$propName] = array($orgValue, $actualValue);
765
                    /* If original collection was exchanged with a non-empty value
766
                     * and $set will be issued, there is no need to $unset it first
767
                     */
768 22
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
769 7
                        continue;
770
                    }
771 16
                    if ($orgValue instanceof PersistentCollection) {
772 14
                        $this->scheduleCollectionDeletion($orgValue);
773 14
                    }
774 16
                    continue;
775
                }
776
777
                // skip equivalent date values
778 138
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
779 29
                    $dateType = Type::getType('date');
780 29
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
781 29
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
782
783 29
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
784 27
                        continue;
785
                    }
786 2
                }
787
788
                // regular field
789 122
                $changeSet[$propName] = array($orgValue, $actualValue);
790 278
            }
791 278
            if ($changeSet) {
792 159
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
793 159
                    ? $changeSet + $this->documentChangeSets[$oid]
794 7
                    : $changeSet;
795
796 159
                $this->originalDocumentData[$oid] = $actualData;
797 159
                $this->scheduleForUpdate($document);
798 159
            }
799
        }
800
801
        // Look for changes in associations of the document
802 558
        $associationMappings = array_filter(
803 558
            $class->associationMappings,
804
            function ($assoc) { return empty($assoc['notSaved']); }
805 558
        );
806
807 558
        foreach ($associationMappings as $mapping) {
808 430
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
809
810 430
            if ($value === null) {
811 286
                continue;
812
            }
813
814 421
            $this->computeAssociationChanges($document, $mapping, $value);
815
816 420
            if (isset($mapping['reference'])) {
817 315
                continue;
818
            }
819
820 328
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
821
822 328
            foreach ($values as $obj) {
823 170
                $oid2 = spl_object_hash($obj);
824
825 170
                if (isset($this->documentChangeSets[$oid2])) {
826 168
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
827
828 168
                    if ( ! $isNewDocument) {
829 69
                        $this->scheduleForUpdate($document);
830 69
                    }
831
832 168
                    break;
833
                }
834 328
            }
835 557
        }
836 557
    }
837
838
    /**
839
     * Computes all the changes that have been done to documents and collections
840
     * since the last commit and stores these changes in the _documentChangeSet map
841
     * temporarily for access by the persisters, until the UoW commit is finished.
842
     */
843 556
    public function computeChangeSets()
844
    {
845 556
        $this->computeScheduleInsertsChangeSets();
846 555
        $this->computeScheduleUpsertsChangeSets();
847
848
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
849 555
        foreach ($this->identityMap as $className => $documents) {
850 555
            $class = $this->dm->getClassMetadata($className);
851 555
            if ($class->isEmbeddedDocument) {
852
                /* we do not want to compute changes to embedded documents up front
853
                 * in case embedded document was replaced and its changeset
854
                 * would corrupt data. Embedded documents' change set will
855
                 * be calculated by reachability from owning document.
856
                 */
857 152
                continue;
858
            }
859
860
            // If change tracking is explicit or happens through notification, then only compute
861
            // changes on document of that type that are explicitly marked for synchronization.
862 555
            switch (true) {
863 555
                case ($class->isChangeTrackingDeferredImplicit()):
864 554
                    $documentsToProcess = $documents;
865 554
                    break;
866
867 3
                case (isset($this->scheduledForDirtyCheck[$className])):
868 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
869 2
                    break;
870
871 3
                default:
872 3
                    $documentsToProcess = array();
873
874 3
            }
875
876 555
            foreach ($documentsToProcess as $document) {
877
                // Ignore uninitialized proxy objects
878 551
                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...
879 5
                    continue;
880
                }
881
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
882 551
                $oid = spl_object_hash($document);
883 551 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...
884 551
                    && ! isset($this->documentUpserts[$oid])
885 551
                    && ! isset($this->documentDeletions[$oid])
886 551
                    && isset($this->documentStates[$oid])
887 551
                ) {
888 263
                    $this->computeChangeSet($class, $document);
889 263
                }
890 555
            }
891 555
        }
892 555
    }
893
894
    /**
895
     * Computes the changes of an association.
896
     *
897
     * @param object $parentDocument
898
     * @param array $assoc
899
     * @param mixed $value The value of the association.
900
     * @throws \InvalidArgumentException
901
     */
902 421
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
903
    {
904 421
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
905 421
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
906 421
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
907
908 421
        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...
909 5
            return;
910
        }
911
912 420
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
913 224
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
914 220
                $this->scheduleCollectionUpdate($value);
915 220
            }
916 224
            $topmostOwner = $this->getOwningDocument($value->getOwner());
917 224
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
918 224
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
919 131
                $value->initialize();
920 131
                foreach ($value->getDeletedDocuments() as $orphan) {
921 17
                    $this->scheduleOrphanRemoval($orphan);
922 131
                }
923 131
            }
924 224
        }
925
926
        // Look through the documents, and in any of their associations,
927
        // for transient (new) documents, recursively. ("Persistence by reachability")
928
        // Unwrap. Uninitialized collections will simply be empty.
929 420
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
930
931 420
        $count = 0;
932 420
        foreach ($unwrappedValue as $key => $entry) {
933 325
            if ( ! is_object($entry)) {
934 1
                throw new \InvalidArgumentException(
935 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
936 1
                );
937
            }
938
939 324
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
940
941 324
            $state = $this->getDocumentState($entry, self::STATE_NEW);
942
943
            // Handle "set" strategy for multi-level hierarchy
944 324
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
945 324
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
946
947 324
            $count++;
948
949
            switch ($state) {
950 324
                case self::STATE_NEW:
951 55
                    if ( ! $assoc['isCascadePersist']) {
952
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
953
                            . " configured to cascade persist operations: " . $this->objToStr($entry) . "."
954
                            . " Explicitly persist the new document or configure cascading persist operations"
955
                            . " on the relationship.");
956
                    }
957
958 55
                    $this->persistNew($targetClass, $entry);
959 55
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
960 55
                    $this->computeChangeSet($targetClass, $entry);
961 55
                    break;
962
963 313
                case self::STATE_MANAGED:
964 313
                    if ($targetClass->isEmbeddedDocument) {
965 154
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
966 154
                        if ($knownParent && $knownParent !== $parentDocument) {
967 6
                            $entry = clone $entry;
968 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
969 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
970 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
971 3
                            } else {
972
                                // must use unwrapped value to not trigger orphan removal
973 6
                                $unwrappedValue[$key] = $entry;
974
                            }
975 6
                            $this->persistNew($targetClass, $entry);
976 6
                        }
977 154
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
978 154
                        $this->computeChangeSet($targetClass, $entry);
979 154
                    }
980 313
                    break;
981
982
                case self::STATE_REMOVED:
983
                    // Consume the $value as array (it's either an array or an ArrayAccess)
984
                    // and remove the element from Collection.
985
                    if ($assoc['type'] === ClassMetadata::MANY) {
986
                        unset($value[$key]);
987
                    }
988
                    break;
989
990
                case self::STATE_DETACHED:
991
                    // Can actually not happen right now as we assume STATE_NEW,
992
                    // so the exception will be raised from the DBAL layer (constraint violation).
993
                    throw new \InvalidArgumentException("A detached document was found through a "
994
                        . "relationship during cascading a persist operation.");
995
996
                default:
997
                    // MANAGED associated documents are already taken into account
998
                    // during changeset calculation anyway, since they are in the identity map.
999
1000
            }
1001 419
        }
1002 419
    }
1003
1004
    /**
1005
     * INTERNAL:
1006
     * Computes the changeset of an individual document, independently of the
1007
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1008
     *
1009
     * The passed document must be a managed document. If the document already has a change set
1010
     * because this method is invoked during a commit cycle then the change sets are added.
1011
     * whereby changes detected in this method prevail.
1012
     *
1013
     * @ignore
1014
     * @param ClassMetadata $class The class descriptor of the document.
1015
     * @param object $document The document for which to (re)calculate the change set.
1016
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1017
     */
1018 7
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1019
    {
1020
        // Ignore uninitialized proxy objects
1021 7
        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...
1022 1
            return;
1023
        }
1024
1025 6
        $oid = spl_object_hash($document);
1026
1027 6
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1028
            throw new \InvalidArgumentException('Document must be managed.');
1029
        }
1030
1031 6
        if ( ! $class->isInheritanceTypeNone()) {
1032 1
            $class = $this->dm->getClassMetadata(get_class($document));
1033 1
        }
1034
1035 6
        $this->computeOrRecomputeChangeSet($class, $document, true);
1036 6
    }
1037
1038
    /**
1039
     * @param ClassMetadata $class
1040
     * @param object $document
1041
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1042
     */
1043 576
    private function persistNew(ClassMetadata $class, $document)
1044
    {
1045 576
        $this->lifecycleEventManager->prePersist($class, $document);
1046 576
        $oid = spl_object_hash($document);
1047 576
        $upsert = false;
1048 576
        if ($class->identifier) {
1049 576
            $idValue = $class->getIdentifierValue($document);
1050 576
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1051
1052 576
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1053 3
                throw new \InvalidArgumentException(sprintf(
1054 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1055 3
                    get_class($document)
1056 3
                ));
1057
            }
1058
1059 575
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1060 1
                throw new \InvalidArgumentException(sprintf(
1061 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1062 1
                    get_class($document)
1063 1
                ));
1064
            }
1065
1066 574
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1067 503
                $idValue = $class->idGenerator->generate($this->dm, $document);
1068 503
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1069 503
                $class->setIdentifierValue($document, $idValue);
1070 503
            }
1071
1072 574
            $this->documentIdentifiers[$oid] = $idValue;
1073 574
        } else {
1074
            // this is for embedded documents without identifiers
1075 143
            $this->documentIdentifiers[$oid] = $oid;
1076
        }
1077
1078 574
        $this->documentStates[$oid] = self::STATE_MANAGED;
1079
1080 574
        if ($upsert) {
1081 81
            $this->scheduleForUpsert($class, $document);
1082 81
        } else {
1083 508
            $this->scheduleForInsert($class, $document);
1084
        }
1085 574
    }
1086
1087
    /**
1088
     * Executes all document insertions for documents of the specified type.
1089
     *
1090
     * @param ClassMetadata $class
1091
     * @param array $documents Array of documents to insert
1092
     * @param array $options Array of options to be used with batchInsert()
1093
     */
1094 487 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...
1095
    {
1096 487
        $persister = $this->getDocumentPersister($class->name);
1097
1098 487
        foreach ($documents as $oid => $document) {
1099 487
            $persister->addInsert($document);
1100 487
            unset($this->documentInsertions[$oid]);
1101 487
        }
1102
1103 487
        $persister->executeInserts($options);
1104
1105 486
        foreach ($documents as $document) {
1106 486
            $this->lifecycleEventManager->postPersist($class, $document);
1107 486
        }
1108 486
    }
1109
1110
    /**
1111
     * Executes all document upserts for documents of the specified type.
1112
     *
1113
     * @param ClassMetadata $class
1114
     * @param array $documents Array of documents to upsert
1115
     * @param array $options Array of options to be used with batchInsert()
1116
     */
1117 78 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...
1118
    {
1119 78
        $persister = $this->getDocumentPersister($class->name);
1120
1121
1122 78
        foreach ($documents as $oid => $document) {
1123 78
            $persister->addUpsert($document);
1124 78
            unset($this->documentUpserts[$oid]);
1125 78
        }
1126
1127 78
        $persister->executeUpserts($options);
1128
1129 78
        foreach ($documents as $document) {
1130 78
            $this->lifecycleEventManager->postPersist($class, $document);
1131 78
        }
1132 78
    }
1133
1134
    /**
1135
     * Executes all document updates for documents of the specified type.
1136
     *
1137
     * @param Mapping\ClassMetadata $class
1138
     * @param array $documents Array of documents to update
1139
     * @param array $options Array of options to be used with update()
1140
     */
1141 218
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1142
    {
1143 218
        $className = $class->name;
1144 218
        $persister = $this->getDocumentPersister($className);
1145
1146 218
        foreach ($documents as $oid => $document) {
1147 218
            $this->lifecycleEventManager->preUpdate($class, $document);
1148
1149
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1150
                $persister->update($document, $options);
1151
            }
1152
1153
            unset($this->documentUpdates[$oid]);
1154
1155
            $this->lifecycleEventManager->postUpdate($class, $document);
1156
        }
1157
    }
1158
1159
    /**
1160
     * Executes all document deletions for documents of the specified type.
1161
     *
1162
     * @param ClassMetadata $class
1163
     * @param array $documents Array of documents to delete
1164
     * @param array $options Array of options to be used with remove()
1165
     */
1166 12
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1167
    {
1168 12
        $persister = $this->getDocumentPersister($class->name);
1169
1170 12
        foreach ($documents as $oid => $document) {
1171 12
            if ( ! $class->isEmbeddedDocument) {
1172 11
                $persister->delete($document, $options);
1173 9
            }
1174
            unset(
1175 10
                $this->documentDeletions[$oid],
1176 10
                $this->documentIdentifiers[$oid],
1177 10
                $this->originalDocumentData[$oid]
1178
            );
1179
1180
            // Clear snapshot information for any referenced PersistentCollection
1181
            // http://www.doctrine-project.org/jira/browse/MODM-95
1182 10
            foreach ($class->associationMappings as $fieldMapping) {
1183 8
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1184 6
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1185 6
                    if ($value instanceof PersistentCollection) {
1186 6
                        $value->clearSnapshot();
1187 6
                    }
1188 6
                }
1189 10
            }
1190
1191
            // Document with this $oid after deletion treated as NEW, even if the $oid
1192
            // is obtained by a new document because the old one went out of scope.
1193 10
            $this->documentStates[$oid] = self::STATE_NEW;
1194
1195 10
            $this->lifecycleEventManager->postRemove($class, $document);
1196 10
        }
1197 10
    }
1198
1199
    /**
1200
     * Schedules a document for insertion into the database.
1201
     * If the document already has an identifier, it will be added to the
1202
     * identity map.
1203
     *
1204
     * @param ClassMetadata $class
1205
     * @param object $document The document to schedule for insertion.
1206
     * @throws \InvalidArgumentException
1207
     */
1208 511
    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...
1209
    {
1210 511
        $oid = spl_object_hash($document);
1211
1212 511
        if (isset($this->documentUpdates[$oid])) {
1213
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1214
        }
1215 511
        if (isset($this->documentDeletions[$oid])) {
1216
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1217
        }
1218 511
        if (isset($this->documentInsertions[$oid])) {
1219
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1220
        }
1221
1222 511
        $this->documentInsertions[$oid] = $document;
1223
1224 511
        if (isset($this->documentIdentifiers[$oid])) {
1225 508
            $this->addToIdentityMap($document);
1226 508
        }
1227 511
    }
1228
1229
    /**
1230
     * Schedules a document for upsert into the database and adds it to the
1231
     * identity map
1232
     *
1233
     * @param ClassMetadata $class
1234
     * @param object $document The document to schedule for upsert.
1235
     * @throws \InvalidArgumentException
1236
     */
1237 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1238
    {
1239 84
        $oid = spl_object_hash($document);
1240
1241 84
        if ($class->isEmbeddedDocument) {
1242
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1243
        }
1244 84
        if (isset($this->documentUpdates[$oid])) {
1245
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1246
        }
1247 84
        if (isset($this->documentDeletions[$oid])) {
1248
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1249
        }
1250 84
        if (isset($this->documentUpserts[$oid])) {
1251
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1252
        }
1253
1254 84
        $this->documentUpserts[$oid] = $document;
1255 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1256 84
        $this->addToIdentityMap($document);
1257 84
    }
1258
1259
    /**
1260
     * Checks whether a document is scheduled for insertion.
1261
     *
1262
     * @param object $document
1263
     * @return boolean
1264
     */
1265 14
    public function isScheduledForInsert($document)
1266
    {
1267 14
        return isset($this->documentInsertions[spl_object_hash($document)]);
1268
    }
1269
1270
    /**
1271
     * Checks whether a document is scheduled for upsert.
1272
     *
1273
     * @param object $document
1274
     * @return boolean
1275
     */
1276 5
    public function isScheduledForUpsert($document)
1277
    {
1278 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1279
    }
1280
1281
    /**
1282
     * Schedules a document for being updated.
1283
     *
1284
     * @param object $document The document to schedule for being updated.
1285
     * @throws \InvalidArgumentException
1286
     */
1287 227
    public function scheduleForUpdate($document)
1288
    {
1289 227
        $oid = spl_object_hash($document);
1290 227
        if ( ! isset($this->documentIdentifiers[$oid])) {
1291
            throw new \InvalidArgumentException("Document has no identity.");
1292
        }
1293
1294 227
        if (isset($this->documentDeletions[$oid])) {
1295
            throw new \InvalidArgumentException("Document is removed.");
1296
        }
1297
1298 227
        if ( ! isset($this->documentUpdates[$oid])
1299 227
            && ! isset($this->documentInsertions[$oid])
1300 227
            && ! isset($this->documentUpserts[$oid])) {
1301 223
            $this->documentUpdates[$oid] = $document;
1302 223
        }
1303 227
    }
1304
1305
    /**
1306
     * Checks whether a document is registered as dirty in the unit of work.
1307
     * Note: Is not very useful currently as dirty documents are only registered
1308
     * at commit time.
1309
     *
1310
     * @param object $document
1311
     * @return boolean
1312
     */
1313 13
    public function isScheduledForUpdate($document)
1314
    {
1315 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1316
    }
1317
1318 1
    public function isScheduledForDirtyCheck($document)
1319
    {
1320 1
        $class = $this->dm->getClassMetadata(get_class($document));
1321 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1322
    }
1323
1324
    /**
1325
     * INTERNAL:
1326
     * Schedules a document for deletion.
1327
     *
1328
     * @param object $document
1329
     */
1330 49
    public function scheduleForDelete($document)
1331
    {
1332 49
        $oid = spl_object_hash($document);
1333
1334 49
        if (isset($this->documentInsertions[$oid])) {
1335 2
            if ($this->isInIdentityMap($document)) {
1336 2
                $this->removeFromIdentityMap($document);
1337 2
            }
1338 2
            unset($this->documentInsertions[$oid]);
1339 2
            return; // document has not been persisted yet, so nothing more to do.
1340
        }
1341
1342 48
        if ( ! $this->isInIdentityMap($document)) {
1343 1
            return; // ignore
1344
        }
1345
1346 47
        $this->removeFromIdentityMap($document);
1347 47
        $this->documentStates[$oid] = self::STATE_REMOVED;
1348
1349 47
        if (isset($this->documentUpdates[$oid])) {
1350
            unset($this->documentUpdates[$oid]);
1351
        }
1352 47
        if ( ! isset($this->documentDeletions[$oid])) {
1353 47
            $this->documentDeletions[$oid] = $document;
1354 47
        }
1355 47
    }
1356
1357
    /**
1358
     * Checks whether a document is registered as removed/deleted with the unit
1359
     * of work.
1360
     *
1361
     * @param object $document
1362
     * @return boolean
1363
     */
1364 6
    public function isScheduledForDelete($document)
1365
    {
1366 6
        return isset($this->documentDeletions[spl_object_hash($document)]);
1367
    }
1368
1369
    /**
1370
     * Checks whether a document is scheduled for insertion, update or deletion.
1371
     *
1372
     * @param $document
1373
     * @return boolean
1374
     */
1375 223
    public function isDocumentScheduled($document)
1376
    {
1377 223
        $oid = spl_object_hash($document);
1378 223
        return isset($this->documentInsertions[$oid]) ||
1379 117
            isset($this->documentUpserts[$oid]) ||
1380 108
            isset($this->documentUpdates[$oid]) ||
1381 223
            isset($this->documentDeletions[$oid]);
1382
    }
1383
1384
    /**
1385
     * INTERNAL:
1386
     * Registers a document in the identity map.
1387
     *
1388
     * Note that documents in a hierarchy are registered with the class name of
1389
     * the root document. Identifiers are serialized before being used as array
1390
     * keys to allow differentiation of equal, but not identical, values.
1391
     *
1392
     * @ignore
1393
     * @param object $document  The document to register.
1394
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1395
     *                  the document in question is already managed.
1396
     */
1397 605
    public function addToIdentityMap($document)
1398
    {
1399 605
        $class = $this->dm->getClassMetadata(get_class($document));
1400 605
        $id = $this->getIdForIdentityMap($document);
1401
1402 605
        if (isset($this->identityMap[$class->name][$id])) {
1403 51
            return false;
1404
        }
1405
1406 605
        $this->identityMap[$class->name][$id] = $document;
1407
1408 605
        if ($document instanceof NotifyPropertyChanged &&
1409 605
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1410 3
            $document->addPropertyChangedListener($this);
1411 3
        }
1412
1413 605
        return true;
1414
    }
1415
1416
    /**
1417
     * Gets the state of a document with regard to the current unit of work.
1418
     *
1419
     * @param object   $document
1420
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1421
     *                         This parameter can be set to improve performance of document state detection
1422
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1423
     *                         is either known or does not matter for the caller of the method.
1424
     * @return int The document state.
1425
     */
1426 579
    public function getDocumentState($document, $assume = null)
1427
    {
1428 579
        $oid = spl_object_hash($document);
1429
1430 579
        if (isset($this->documentStates[$oid])) {
1431 348
            return $this->documentStates[$oid];
1432
        }
1433
1434 579
        $class = $this->dm->getClassMetadata(get_class($document));
1435
1436 579
        if ($class->isEmbeddedDocument) {
1437 176
            return self::STATE_NEW;
1438
        }
1439
1440 576
        if ($assume !== null) {
1441 573
            return $assume;
1442
        }
1443
1444
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1445
         * known. Note that you cannot remember the NEW or DETACHED state in
1446
         * _documentStates since the UoW does not hold references to such
1447
         * objects and the object hash can be reused. More generally, because
1448
         * the state may "change" between NEW/DETACHED without the UoW being
1449
         * aware of it.
1450
         */
1451 4
        $id = $class->getIdentifierObject($document);
1452
1453 4
        if ($id === null) {
1454 2
            return self::STATE_NEW;
1455
        }
1456
1457
        // Check for a version field, if available, to avoid a DB lookup.
1458 2
        if ($class->isVersioned) {
1459
            return ($class->getFieldValue($document, $class->versionField))
1460
                ? self::STATE_DETACHED
1461
                : self::STATE_NEW;
1462
        }
1463
1464
        // Last try before DB lookup: check the identity map.
1465 2
        if ($this->tryGetById($id, $class)) {
1466 1
            return self::STATE_DETACHED;
1467
        }
1468
1469
        // DB lookup
1470 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1471 1
            return self::STATE_DETACHED;
1472
        }
1473
1474 1
        return self::STATE_NEW;
1475
    }
1476
1477
    /**
1478
     * INTERNAL:
1479
     * Removes a document from the identity map. This effectively detaches the
1480
     * document from the persistence management of Doctrine.
1481
     *
1482
     * @ignore
1483
     * @param object $document
1484
     * @throws \InvalidArgumentException
1485
     * @return boolean
1486
     */
1487 58
    public function removeFromIdentityMap($document)
1488
    {
1489 58
        $oid = spl_object_hash($document);
1490
1491
        // Check if id is registered first
1492 58
        if ( ! isset($this->documentIdentifiers[$oid])) {
1493
            return false;
1494
        }
1495
1496 58
        $class = $this->dm->getClassMetadata(get_class($document));
1497 58
        $id = $this->getIdForIdentityMap($document);
1498
1499 58
        if (isset($this->identityMap[$class->name][$id])) {
1500 58
            unset($this->identityMap[$class->name][$id]);
1501 58
            $this->documentStates[$oid] = self::STATE_DETACHED;
1502 58
            return true;
1503
        }
1504
1505
        return false;
1506
    }
1507
1508
    /**
1509
     * INTERNAL:
1510
     * Gets a document in the identity map by its identifier hash.
1511
     *
1512
     * @ignore
1513
     * @param mixed         $id    Document identifier
1514
     * @param ClassMetadata $class Document class
1515
     * @return object
1516
     * @throws InvalidArgumentException if the class does not have an identifier
1517
     */
1518 27
    public function getById($id, ClassMetadata $class)
1519
    {
1520 27
        if ( ! $class->identifier) {
1521
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1522
        }
1523
1524 27
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1525
1526 27
        return $this->identityMap[$class->name][$serializedId];
1527
    }
1528
1529
    /**
1530
     * INTERNAL:
1531
     * Tries to get a document by its identifier hash. If no document is found
1532
     * for the given hash, FALSE is returned.
1533
     *
1534
     * @ignore
1535
     * @param mixed         $id    Document identifier
1536
     * @param ClassMetadata $class Document class
1537
     * @return mixed The found document or FALSE.
1538
     * @throws InvalidArgumentException if the class does not have an identifier
1539
     */
1540 253
    public function tryGetById($id, ClassMetadata $class)
1541
    {
1542 253
        if ( ! $class->identifier) {
1543
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1544
        }
1545
1546 253
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1547
1548 253
        return isset($this->identityMap[$class->name][$serializedId]) ?
1549 253
            $this->identityMap[$class->name][$serializedId] : false;
1550
    }
1551
1552
    /**
1553
     * Schedules a document for dirty-checking at commit-time.
1554
     *
1555
     * @param object $document The document to schedule for dirty-checking.
1556
     * @todo Rename: scheduleForSynchronization
1557
     */
1558 2
    public function scheduleForDirtyCheck($document)
1559
    {
1560 2
        $class = $this->dm->getClassMetadata(get_class($document));
1561 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1562 2
    }
1563
1564
    /**
1565
     * Checks whether a document is registered in the identity map.
1566
     *
1567
     * @param object $document
1568
     * @return boolean
1569
     */
1570 56
    public function isInIdentityMap($document)
1571
    {
1572 56
        $oid = spl_object_hash($document);
1573
1574 56
        if ( ! isset($this->documentIdentifiers[$oid])) {
1575 4
            return false;
1576
        }
1577
1578 55
        $class = $this->dm->getClassMetadata(get_class($document));
1579 55
        $id = $this->getIdForIdentityMap($document);
1580
1581 55
        return isset($this->identityMap[$class->name][$id]);
1582
    }
1583
1584
    /**
1585
     * @param object $document
1586
     * @return string
1587
     */
1588 605
    private function getIdForIdentityMap($document)
1589
    {
1590 605
        $class = $this->dm->getClassMetadata(get_class($document));
1591
1592 605
        if ( ! $class->identifier) {
1593 146
            $id = spl_object_hash($document);
1594 146
        } else {
1595 604
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1596 604
            $id = serialize($class->getDatabaseIdentifierValue($id));
1597
        }
1598
1599 605
        return $id;
1600
    }
1601
1602
    /**
1603
     * INTERNAL:
1604
     * Checks whether an identifier exists in the identity map.
1605
     *
1606
     * @ignore
1607
     * @param string $id
1608
     * @param string $rootClassName
1609
     * @return boolean
1610
     */
1611
    public function containsId($id, $rootClassName)
1612
    {
1613
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1614
    }
1615
1616
    /**
1617
     * Persists a document as part of the current unit of work.
1618
     *
1619
     * @param object $document The document to persist.
1620
     * @throws MongoDBException If trying to persist MappedSuperclass.
1621
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1622
     */
1623 574
    public function persist($document)
1624
    {
1625 574
        $class = $this->dm->getClassMetadata(get_class($document));
1626 574
        if ($class->isMappedSuperclass) {
1627 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1628
        }
1629 573
        $visited = array();
1630 573
        $this->doPersist($document, $visited);
1631 569
    }
1632
1633
    /**
1634
     * Saves a document as part of the current unit of work.
1635
     * This method is internally called during save() cascades as it tracks
1636
     * the already visited documents to prevent infinite recursions.
1637
     *
1638
     * NOTE: This method always considers documents that are not yet known to
1639
     * this UnitOfWork as NEW.
1640
     *
1641
     * @param object $document The document to persist.
1642
     * @param array $visited The already visited documents.
1643
     * @throws \InvalidArgumentException
1644
     * @throws MongoDBException
1645
     */
1646 573
    private function doPersist($document, array &$visited)
1647
    {
1648 573
        $oid = spl_object_hash($document);
1649 573
        if (isset($visited[$oid])) {
1650 24
            return; // Prevent infinite recursion
1651
        }
1652
1653 573
        $visited[$oid] = $document; // Mark visited
1654
1655 573
        $class = $this->dm->getClassMetadata(get_class($document));
1656
1657 573
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1658
        switch ($documentState) {
1659 573
            case self::STATE_MANAGED:
1660
                // Nothing to do, except if policy is "deferred explicit"
1661 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1662
                    $this->scheduleForDirtyCheck($document);
1663
                }
1664 44
                break;
1665 573
            case self::STATE_NEW:
1666 573
                $this->persistNew($class, $document);
1667 571
                break;
1668
1669 2
            case self::STATE_REMOVED:
1670
                // Document becomes managed again
1671 2
                unset($this->documentDeletions[$oid]);
1672
1673 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1674 2
                break;
1675
1676
            case self::STATE_DETACHED:
1677
                throw new \InvalidArgumentException(
1678
                    "Behavior of persist() for a detached document is not yet defined.");
1679
1680
            default:
1681
                throw MongoDBException::invalidDocumentState($documentState);
1682
        }
1683
1684 571
        $this->cascadePersist($document, $visited);
1685 569
    }
1686
1687
    /**
1688
     * Deletes a document as part of the current unit of work.
1689
     *
1690
     * @param object $document The document to remove.
1691
     */
1692 48
    public function remove($document)
1693
    {
1694 48
        $visited = array();
1695 48
        $this->doRemove($document, $visited);
1696 48
    }
1697
1698
    /**
1699
     * Deletes a document as part of the current unit of work.
1700
     *
1701
     * This method is internally called during delete() cascades as it tracks
1702
     * the already visited documents to prevent infinite recursions.
1703
     *
1704
     * @param object $document The document to delete.
1705
     * @param array $visited The map of the already visited documents.
1706
     * @throws MongoDBException
1707
     */
1708 48
    private function doRemove($document, array &$visited)
1709
    {
1710 48
        $oid = spl_object_hash($document);
1711 48
        if (isset($visited[$oid])) {
1712 1
            return; // Prevent infinite recursion
1713
        }
1714
1715 48
        $visited[$oid] = $document; // mark visited
1716
1717
        /* Cascade first, because scheduleForDelete() removes the entity from
1718
         * the identity map, which can cause problems when a lazy Proxy has to
1719
         * be initialized for the cascade operation.
1720
         */
1721 48
        $this->cascadeRemove($document, $visited);
1722
1723 48
        $class = $this->dm->getClassMetadata(get_class($document));
1724 48
        $documentState = $this->getDocumentState($document);
1725
        switch ($documentState) {
1726 48
            case self::STATE_NEW:
1727 48
            case self::STATE_REMOVED:
1728
                // nothing to do
1729 1
                break;
1730 48
            case self::STATE_MANAGED:
1731 48
                $this->lifecycleEventManager->preRemove($class, $document);
1732 48
                $this->scheduleForDelete($document);
1733 48
                break;
1734
            case self::STATE_DETACHED:
1735
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1736
            default:
1737
                throw MongoDBException::invalidDocumentState($documentState);
1738
        }
1739 48
    }
1740
1741
    /**
1742
     * Merges the state of the given detached document into this UnitOfWork.
1743
     *
1744
     * @param object $document
1745
     * @return object The managed copy of the document.
1746
     */
1747 13
    public function merge($document)
1748
    {
1749 13
        $visited = array();
1750
1751 13
        return $this->doMerge($document, $visited);
1752
    }
1753
1754
    /**
1755
     * Executes a merge operation on a document.
1756
     *
1757
     * @param object      $document
1758
     * @param array       $visited
1759
     * @param object|null $prevManagedCopy
1760
     * @param array|null  $assoc
1761
     *
1762
     * @return object The managed copy of the document.
1763
     *
1764
     * @throws InvalidArgumentException If the entity instance is NEW.
1765
     * @throws LockException If the document uses optimistic locking through a
1766
     *                       version attribute and the version check against the
1767
     *                       managed copy fails.
1768
     */
1769 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1770
    {
1771 13
        $oid = spl_object_hash($document);
1772
1773 13
        if (isset($visited[$oid])) {
1774 1
            return $visited[$oid]; // Prevent infinite recursion
1775
        }
1776
1777 13
        $visited[$oid] = $document; // mark visited
1778
1779 13
        $class = $this->dm->getClassMetadata(get_class($document));
1780
1781
        /* First we assume DETACHED, although it can still be NEW but we can
1782
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1783
         * an identity, we need to fetch it from the DB anyway in order to
1784
         * merge. MANAGED documents are ignored by the merge operation.
1785
         */
1786 13
        $managedCopy = $document;
1787
1788 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1789 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1790
                $document->__load();
1791
            }
1792
1793
            // Try to look the document up in the identity map.
1794 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1795
1796 13
            if ($id === null) {
1797
                // If there is no identifier, it is actually NEW.
1798 5
                $managedCopy = $class->newInstance();
1799 5
                $this->persistNew($class, $managedCopy);
1800 5
            } else {
1801 10
                $managedCopy = $this->tryGetById($id, $class);
1802
1803 10
                if ($managedCopy) {
1804
                    // We have the document in memory already, just make sure it is not removed.
1805 4
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1806
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1807
                    }
1808 4
                } else {
1809
                    // We need to fetch the managed copy in order to merge.
1810 7
                    $managedCopy = $this->dm->find($class->name, $id);
1811
                }
1812
1813 10
                if ($managedCopy === null) {
1814
                    // If the identifier is ASSIGNED, it is NEW
1815
                    $managedCopy = $class->newInstance();
1816
                    $class->setIdentifierValue($managedCopy, $id);
1817
                    $this->persistNew($class, $managedCopy);
1818
                } else {
1819 10
                    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...
1820
                        $managedCopy->__load();
1821
                    }
1822
                }
1823
            }
1824
1825 13
            if ($class->isVersioned) {
1826
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1827
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1828
1829
                // Throw exception if versions don't match
1830
                if ($managedCopyVersion != $documentVersion) {
1831
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1832
                }
1833
            }
1834
1835
            // Merge state of $document into existing (managed) document
1836 13
            foreach ($class->reflClass->getProperties() as $prop) {
1837 13
                $name = $prop->name;
1838 13
                $prop->setAccessible(true);
1839 13
                if ( ! isset($class->associationMappings[$name])) {
1840 13
                    if ( ! $class->isIdentifier($name)) {
1841 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
1842 13
                    }
1843 13
                } else {
1844 13
                    $assoc2 = $class->associationMappings[$name];
1845
1846 13
                    if ($assoc2['type'] === 'one') {
1847 5
                        $other = $prop->getValue($document);
1848
1849 5
                        if ($other === null) {
1850 2
                            $prop->setValue($managedCopy, null);
1851 5
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1852
                            // Do not merge fields marked lazy that have not been fetched
1853 1
                            continue;
1854 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1855
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1856
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1857
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1858
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1859
                                $relatedId = $targetClass->getIdentifierObject($other);
1860
1861
                                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...
1862
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1863
                                } else {
1864
                                    $other = $this
1865
                                        ->dm
1866
                                        ->getProxyFactory()
1867
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1868
                                    $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...
1869
                                }
1870
                            }
1871
1872
                            $prop->setValue($managedCopy, $other);
1873
                        }
1874 4
                    } else {
1875 10
                        $mergeCol = $prop->getValue($document);
1876
1877 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
1878
                            /* Do not merge fields marked lazy that have not
1879
                             * been fetched. Keep the lazy persistent collection
1880
                             * of the managed copy.
1881
                             */
1882 3
                            continue;
1883
                        }
1884
1885 7
                        $managedCol = $prop->getValue($managedCopy);
1886
1887 7
                        if ( ! $managedCol) {
1888 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
1889 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1890 2
                            $prop->setValue($managedCopy, $managedCol);
1891 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1892 2
                        }
1893
1894
                        /* Note: do not process association's target documents.
1895
                         * They will be handled during the cascade. Initialize
1896
                         * and, if necessary, clear $managedCol for now.
1897
                         */
1898 7
                        if ($assoc2['isCascadeMerge']) {
1899 7
                            $managedCol->initialize();
1900
1901
                            // If $managedCol differs from the merged collection, clear and set dirty
1902 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1903 2
                                $managedCol->unwrap()->clear();
1904 2
                                $managedCol->setDirty(true);
1905
1906 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1907
                                    $this->scheduleForDirtyCheck($managedCopy);
1908
                                }
1909 2
                            }
1910 7
                        }
1911
                    }
1912
                }
1913
1914 13
                if ($class->isChangeTrackingNotify()) {
1915
                    // Just treat all properties as changed, there is no other choice.
1916
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1917
                }
1918 13
            }
1919
1920 13
            if ($class->isChangeTrackingDeferredExplicit()) {
1921
                $this->scheduleForDirtyCheck($document);
1922
            }
1923 13
        }
1924
1925 13
        if ($prevManagedCopy !== null) {
1926 6
            $assocField = $assoc['fieldName'];
1927 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1928
1929 6
            if ($assoc['type'] === 'one') {
1930 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1931 2
            } else {
1932 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1933
1934 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1935 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1936 1
                }
1937
            }
1938 6
        }
1939
1940
        // Mark the managed copy visited as well
1941 13
        $visited[spl_object_hash($managedCopy)] = true;
1942
1943 13
        $this->cascadeMerge($document, $managedCopy, $visited);
1944
1945 13
        return $managedCopy;
1946
    }
1947
1948
    /**
1949
     * Detaches a document from the persistence management. It's persistence will
1950
     * no longer be managed by Doctrine.
1951
     *
1952
     * @param object $document The document to detach.
1953
     */
1954 9
    public function detach($document)
1955
    {
1956 9
        $visited = array();
1957 9
        $this->doDetach($document, $visited);
1958 9
    }
1959
1960
    /**
1961
     * Executes a detach operation on the given document.
1962
     *
1963
     * @param object $document
1964
     * @param array $visited
1965
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1966
     */
1967 12
    private function doDetach($document, array &$visited)
1968
    {
1969 12
        $oid = spl_object_hash($document);
1970 12
        if (isset($visited[$oid])) {
1971 4
            return; // Prevent infinite recursion
1972
        }
1973
1974 12
        $visited[$oid] = $document; // mark visited
1975
1976 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1977 12
            case self::STATE_MANAGED:
1978 12
                $this->removeFromIdentityMap($document);
1979 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1980 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1981 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1982 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1983 12
                    $this->hasScheduledCollections[$oid]);
1984 12
                break;
1985 4
            case self::STATE_NEW:
1986 4
            case self::STATE_DETACHED:
1987 4
                return;
1988 12
        }
1989
1990 12
        $this->cascadeDetach($document, $visited);
1991 12
    }
1992
1993
    /**
1994
     * Refreshes the state of the given document from the database, overwriting
1995
     * any local, unpersisted changes.
1996
     *
1997
     * @param object $document The document to refresh.
1998
     * @throws \InvalidArgumentException If the document is not MANAGED.
1999
     */
2000 16
    public function refresh($document)
2001
    {
2002 16
        $visited = array();
2003 16
        $this->doRefresh($document, $visited);
2004 15
    }
2005
2006
    /**
2007
     * Executes a refresh operation on a document.
2008
     *
2009
     * @param object $document The document to refresh.
2010
     * @param array $visited The already visited documents during cascades.
2011
     * @throws \InvalidArgumentException If the document is not MANAGED.
2012
     */
2013 16
    private function doRefresh($document, array &$visited)
2014
    {
2015 16
        $oid = spl_object_hash($document);
2016 16
        if (isset($visited[$oid])) {
2017
            return; // Prevent infinite recursion
2018
        }
2019
2020 16
        $visited[$oid] = $document; // mark visited
2021
2022 16
        $class = $this->dm->getClassMetadata(get_class($document));
2023
2024 16
        if ( ! $class->isEmbeddedDocument) {
2025 16
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2026 15
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2027 15
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2028 15
            } else {
2029 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2030
            }
2031 15
        }
2032
2033 15
        $this->cascadeRefresh($document, $visited);
2034 15
    }
2035
2036
    /**
2037
     * Cascades a refresh operation to associated documents.
2038
     *
2039
     * @param object $document
2040
     * @param array $visited
2041
     */
2042 15
    private function cascadeRefresh($document, array &$visited)
2043
    {
2044 15
        $class = $this->dm->getClassMetadata(get_class($document));
2045
2046 15
        $associationMappings = array_filter(
2047 15
            $class->associationMappings,
2048
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2049 15
        );
2050
2051 15
        foreach ($associationMappings as $mapping) {
2052 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2053 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2054 11
                if ($relatedDocuments instanceof PersistentCollection) {
2055
                    // Unwrap so that foreach() does not initialize
2056 11
                    $relatedDocuments = $relatedDocuments->unwrap();
2057 11
                }
2058 11
                foreach ($relatedDocuments as $relatedDocument) {
2059
                    $this->doRefresh($relatedDocument, $visited);
2060 11
                }
2061 11
            } elseif ($relatedDocuments !== null) {
2062
                $this->doRefresh($relatedDocuments, $visited);
2063
            }
2064 15
        }
2065 15
    }
2066
2067
    /**
2068
     * Cascades a detach operation to associated documents.
2069
     *
2070
     * @param object $document
2071
     * @param array $visited
2072
     */
2073 12 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...
2074
    {
2075 12
        $class = $this->dm->getClassMetadata(get_class($document));
2076 12
        foreach ($class->fieldMappings as $mapping) {
2077 12
            if ( ! $mapping['isCascadeDetach']) {
2078 12
                continue;
2079
            }
2080 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2081 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2082 7
                if ($relatedDocuments instanceof PersistentCollection) {
2083
                    // Unwrap so that foreach() does not initialize
2084 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2085 6
                }
2086 7
                foreach ($relatedDocuments as $relatedDocument) {
2087 5
                    $this->doDetach($relatedDocument, $visited);
2088 7
                }
2089 7
            } elseif ($relatedDocuments !== null) {
2090 5
                $this->doDetach($relatedDocuments, $visited);
2091 5
            }
2092 12
        }
2093 12
    }
2094
    /**
2095
     * Cascades a merge operation to associated documents.
2096
     *
2097
     * @param object $document
2098
     * @param object $managedCopy
2099
     * @param array $visited
2100
     */
2101 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2102
    {
2103 13
        $class = $this->dm->getClassMetadata(get_class($document));
2104
2105 13
        $associationMappings = array_filter(
2106 13
            $class->associationMappings,
2107
            function ($assoc) { return $assoc['isCascadeMerge']; }
2108 13
        );
2109
2110 13
        foreach ($associationMappings as $assoc) {
2111 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2112
2113 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2114 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2115
                    // Collections are the same, so there is nothing to do
2116
                    continue;
2117
                }
2118
2119 8
                if ($relatedDocuments instanceof PersistentCollection) {
2120
                    // Unwrap so that foreach() does not initialize
2121 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2122 6
                }
2123
2124 8
                foreach ($relatedDocuments as $relatedDocument) {
2125 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2126 8
                }
2127 12
            } elseif ($relatedDocuments !== null) {
2128 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2129 3
            }
2130 13
        }
2131 13
    }
2132
2133
    /**
2134
     * Cascades the save operation to associated documents.
2135
     *
2136
     * @param object $document
2137
     * @param array $visited
2138
     */
2139 571
    private function cascadePersist($document, array &$visited)
2140
    {
2141 571
        $class = $this->dm->getClassMetadata(get_class($document));
2142
2143 571
        $associationMappings = array_filter(
2144 571
            $class->associationMappings,
2145
            function ($assoc) { return $assoc['isCascadePersist']; }
2146 571
        );
2147
2148 571
        foreach ($associationMappings as $fieldName => $mapping) {
2149 391
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2150
2151 391
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2152 339
                if ($relatedDocuments instanceof PersistentCollection) {
2153 17
                    if ($relatedDocuments->getOwner() !== $document) {
2154 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2155 2
                    }
2156
                    // Unwrap so that foreach() does not initialize
2157 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2158 17
                }
2159
2160 339
                $count = 0;
2161 339
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2162 186
                    if ( ! empty($mapping['embedded'])) {
2163 112
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2164 112
                        if ($knownParent && $knownParent !== $document) {
2165 4
                            $relatedDocument = clone $relatedDocument;
2166 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2167 4
                        }
2168 112
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2169 112
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2170 112
                    }
2171 186
                    $this->doPersist($relatedDocument, $visited);
2172 338
                }
2173 391
            } elseif ($relatedDocuments !== null) {
2174 120
                if ( ! empty($mapping['embedded'])) {
2175 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2176 66
                    if ($knownParent && $knownParent !== $document) {
2177 5
                        $relatedDocuments = clone $relatedDocuments;
2178 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2179 5
                    }
2180 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2181 66
                }
2182 120
                $this->doPersist($relatedDocuments, $visited);
2183 119
            }
2184 570
        }
2185 569
    }
2186
2187
    /**
2188
     * Cascades the delete operation to associated documents.
2189
     *
2190
     * @param object $document
2191
     * @param array $visited
2192
     */
2193 48 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...
2194
    {
2195 48
        $class = $this->dm->getClassMetadata(get_class($document));
2196 48
        foreach ($class->fieldMappings as $mapping) {
2197 48
            if ( ! $mapping['isCascadeRemove']) {
2198 48
                continue;
2199
            }
2200 25
            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...
2201 2
                $document->__load();
2202 2
            }
2203
2204 25
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2205 25
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2206
                // If its a PersistentCollection initialization is intended! No unwrap!
2207 16
                foreach ($relatedDocuments as $relatedDocument) {
2208 6
                    $this->doRemove($relatedDocument, $visited);
2209 16
                }
2210 25
            } elseif ($relatedDocuments !== null) {
2211 10
                $this->doRemove($relatedDocuments, $visited);
2212 10
            }
2213 48
        }
2214 48
    }
2215
2216
    /**
2217
     * Acquire a lock on the given document.
2218
     *
2219
     * @param object $document
2220
     * @param int $lockMode
2221
     * @param int $lockVersion
2222
     * @throws LockException
2223
     * @throws \InvalidArgumentException
2224
     */
2225 9
    public function lock($document, $lockMode, $lockVersion = null)
2226
    {
2227 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2228 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2229
        }
2230
2231 8
        $documentName = get_class($document);
2232 8
        $class = $this->dm->getClassMetadata($documentName);
2233
2234 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2235 3
            if ( ! $class->isVersioned) {
2236 1
                throw LockException::notVersioned($documentName);
2237
            }
2238
2239 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...
2240 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2241 2
                if ($documentVersion != $lockVersion) {
2242 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2243
                }
2244 1
            }
2245 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2246 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2247 5
        }
2248 6
    }
2249
2250
    /**
2251
     * Releases a lock on the given document.
2252
     *
2253
     * @param object $document
2254
     * @throws \InvalidArgumentException
2255
     */
2256 1
    public function unlock($document)
2257
    {
2258 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2259
            throw new \InvalidArgumentException("Document is not MANAGED.");
2260
        }
2261 1
        $documentName = get_class($document);
2262 1
        $this->getDocumentPersister($documentName)->unlock($document);
2263 1
    }
2264
2265
    /**
2266
     * Clears the UnitOfWork.
2267
     *
2268
     * @param string|null $documentName if given, only documents of this type will get detached.
2269
     */
2270 347
    public function clear($documentName = null)
2271
    {
2272 347
        if ($documentName === null) {
2273 344
            $this->identityMap =
2274 344
            $this->documentIdentifiers =
2275 344
            $this->originalDocumentData =
2276 344
            $this->documentChangeSets =
2277 344
            $this->documentStates =
2278 344
            $this->scheduledForDirtyCheck =
2279 344
            $this->documentInsertions =
2280 344
            $this->documentUpserts =
2281 344
            $this->documentUpdates =
2282 344
            $this->documentDeletions =
2283 344
            $this->collectionUpdates =
2284 344
            $this->collectionDeletions =
2285 344
            $this->parentAssociations =
2286 344
            $this->orphanRemovals = 
2287 344
            $this->hasScheduledCollections = array();
2288 344
        } else {
2289 3
            $visited = array();
2290 3
            foreach ($this->identityMap as $className => $documents) {
2291 3
                if ($className === $documentName) {
2292 3
                    foreach ($documents as $document) {
2293 3
                        $this->doDetach($document, $visited);
2294 3
                    }
2295 3
                }
2296 3
            }
2297
        }
2298
2299 347 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...
2300
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2301
        }
2302 347
    }
2303
2304
    /**
2305
     * INTERNAL:
2306
     * Schedules an embedded document for removal. The remove() operation will be
2307
     * invoked on that document at the beginning of the next commit of this
2308
     * UnitOfWork.
2309
     *
2310
     * @ignore
2311
     * @param object $document
2312
     */
2313 35
    public function scheduleOrphanRemoval($document)
2314
    {
2315 35
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2316 35
    }
2317
2318
    /**
2319
     * INTERNAL:
2320
     * Unschedules an embedded or referenced object for removal.
2321
     *
2322
     * @ignore
2323
     * @param object $document
2324
     */
2325 94
    public function unscheduleOrphanRemoval($document)
2326
    {
2327 94
        $oid = spl_object_hash($document);
2328 94
        if (isset($this->orphanRemovals[$oid])) {
2329 1
            unset($this->orphanRemovals[$oid]);
2330 1
        }
2331 94
    }
2332
2333
    /**
2334
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2335
     *  1) sets owner if it was cloned
2336
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2337
     *  3) NOP if state is OK
2338
     * Returned collection should be used from now on (only important with 2nd point)
2339
     *
2340
     * @param PersistentCollection $coll
2341
     * @param object $document
2342
     * @param ClassMetadata $class
2343
     * @param string $propName
2344
     * @return PersistentCollection
2345
     */
2346 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2347
    {
2348 8
        $owner = $coll->getOwner();
2349 8
        if ($owner === null) { // cloned
2350 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2351 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2352 2
            if ( ! $coll->isInitialized()) {
2353 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2354 1
            }
2355 2
            $newValue = clone $coll;
2356 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2357 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2358 2
            if ($this->isScheduledForUpdate($document)) {
2359
                // @todo following line should be superfluous once collections are stored in change sets
2360
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2361
            }
2362 2
            return $newValue;
2363
        }
2364 6
        return $coll;
2365
    }
2366
2367
    /**
2368
     * INTERNAL:
2369
     * Schedules a complete collection for removal when this UnitOfWork commits.
2370
     *
2371
     * @param PersistentCollection $coll
2372
     */
2373 28
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2374
    {
2375 28
        $oid = spl_object_hash($coll);
2376 28
        unset($this->collectionUpdates[$oid]);
2377 28
        if ( ! isset($this->collectionDeletions[$oid])) {
2378 28
            $this->collectionDeletions[$oid] = $coll;
2379 28
            $this->scheduleCollectionOwner($coll);
2380 28
        }
2381 28
    }
2382
2383
    /**
2384
     * Checks whether a PersistentCollection is scheduled for deletion.
2385
     *
2386
     * @param PersistentCollection $coll
2387
     * @return boolean
2388
     */
2389 25
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2390
    {
2391 25
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2392
    }
2393
    
2394
    /**
2395
     * INTERNAL:
2396
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2397
     * 
2398
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2399
     */
2400 203 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollection $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...
2401
    {
2402 203
        $oid = spl_object_hash($coll);
2403 203
        if (isset($this->collectionDeletions[$oid])) {
2404 4
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2405 4
            unset($this->collectionDeletions[$oid]);
2406 4
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2407 4
        }
2408 203
    }
2409
2410
    /**
2411
     * INTERNAL:
2412
     * Schedules a collection for update when this UnitOfWork commits.
2413
     *
2414
     * @param PersistentCollection $coll
2415
     */
2416 220
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2417
    {
2418 220
        $mapping = $coll->getMapping();
2419 220
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2420
            /* There is no need to $unset collection if it will be $set later
2421
             * This is NOP if collection is not scheduled for deletion
2422
             */
2423 40
            $this->unscheduleCollectionDeletion($coll);
2424 40
        }
2425 220
        $oid = spl_object_hash($coll);
2426 220
        if ( ! isset($this->collectionUpdates[$oid])) {
2427 220
            $this->collectionUpdates[$oid] = $coll;
2428 220
            $this->scheduleCollectionOwner($coll);
2429 220
        }
2430 220
    }
2431
    
2432
    /**
2433
     * INTERNAL:
2434
     * Unschedules a collection from being updated when this UnitOfWork commits.
2435
     * 
2436
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2437
     */
2438 199 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollection $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...
2439
    {
2440 199
        $oid = spl_object_hash($coll);
2441 199
        if (isset($this->collectionUpdates[$oid])) {
2442 191
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2443 191
            unset($this->collectionUpdates[$oid]);
2444 191
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2445 191
        }
2446 199
    }
2447
    
2448
    /**
2449
     * Checks whether a PersistentCollection is scheduled for update.
2450
     *
2451
     * @param PersistentCollection $coll
2452
     * @return boolean
2453
     */
2454 18
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2455
    {
2456 18
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2457
    }
2458
2459
    /**
2460
     * INTERNAL:
2461
     * Gets PersistentCollections that have been visited during computing change
2462
     * set of $document
2463
     *
2464
     * @param object $document
2465
     * @return PersistentCollection[]
2466
     */
2467 539
    public function getVisitedCollections($document)
2468
    {
2469 539
        $oid = spl_object_hash($document);
2470 539
        return isset($this->visitedCollections[$oid])
2471 539
                ? $this->visitedCollections[$oid]
2472 539
                : array();
2473
    }
2474
    
2475
    /**
2476
     * INTERNAL:
2477
     * Gets PersistentCollections that are scheduled to update and related to $document
2478
     * 
2479
     * @param object $document
2480
     * @return array
2481
     */
2482 539
    public function getScheduledCollections($document)
2483
    {
2484 539
        $oid = spl_object_hash($document);
2485 539
        return isset($this->hasScheduledCollections[$oid]) 
2486 539
                ? $this->hasScheduledCollections[$oid]
2487 539
                : array();
2488
    }
2489
    
2490
    /**
2491
     * Checks whether the document is related to a PersistentCollection
2492
     * scheduled for update or deletion.
2493
     *
2494
     * @param object $document
2495
     * @return boolean
2496
     */
2497
    public function hasScheduledCollections($document)
2498
    {
2499
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2500
    }
2501
    
2502
    /**
2503
     * Marks the PersistentCollection's top-level owner as having a relation to
2504
     * a collection scheduled for update or deletion.
2505
     *
2506
     * If the owner is not scheduled for any lifecycle action, it will be
2507
     * scheduled for update to ensure that versioning takes place if necessary.
2508
     *
2509
     * If the collection is nested within atomic collection, it is immediately
2510
     * unscheduled and atomic one is scheduled for update instead. This makes
2511
     * calculating update data way easier.
2512
     * 
2513
     * @param PersistentCollection $coll
2514
     */
2515 222
    private function scheduleCollectionOwner(PersistentCollection $coll)
2516
    {
2517 222
        $document = $this->getOwningDocument($coll->getOwner());
2518 222
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2519
2520 222
        if ($document !== $coll->getOwner()) {
2521 22
            $parent = $coll->getOwner();
2522 22
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2523 22
                list($mapping, $parent, ) = $parentAssoc;
2524 22
            }
2525 22
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2526 7
                $class = $this->dm->getClassMetadata(get_class($document));
2527 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2528 7
                $this->scheduleCollectionUpdate($atomicCollection);
2529 7
                $this->unscheduleCollectionDeletion($coll);
2530 7
                $this->unscheduleCollectionUpdate($coll);
2531 7
            }
2532 22
        }
2533
2534 222
        if ( ! $this->isDocumentScheduled($document)) {
2535 90
            $this->scheduleForUpdate($document);
2536 90
        }
2537 222
    }
2538
2539
    /**
2540
     * Get the top-most owning document of a given document
2541
     *
2542
     * If a top-level document is provided, that same document will be returned.
2543
     * For an embedded document, we will walk through parent associations until
2544
     * we find a top-level document.
2545
     *
2546
     * @param object $document
2547
     * @throws \UnexpectedValueException when a top-level document could not be found
2548
     * @return object
2549
     */
2550 224
    public function getOwningDocument($document)
2551
    {
2552 224
        $class = $this->dm->getClassMetadata(get_class($document));
2553 224
        while ($class->isEmbeddedDocument) {
2554 37
            $parentAssociation = $this->getParentAssociation($document);
2555
2556 37
            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...
2557
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2558
            }
2559
2560 37
            list(, $document, ) = $parentAssociation;
2561 37
            $class = $this->dm->getClassMetadata(get_class($document));
2562 37
        }
2563
2564 224
        return $document;
2565
    }
2566
2567
    /**
2568
     * Gets the class name for an association (embed or reference) with respect
2569
     * to any discriminator value.
2570
     *
2571
     * @param array      $mapping Field mapping for the association
2572
     * @param array|null $data    Data for the embedded document or reference
2573
     */
2574 174
    public function getClassNameForAssociation(array $mapping, $data)
2575
    {
2576 174
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2577
2578 174
        $discriminatorValue = null;
2579 174
        if (isset($discriminatorField, $data[$discriminatorField])) {
2580 21
            $discriminatorValue = $data[$discriminatorField];
2581 174
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2582
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2583
        }
2584
2585 174
        if ($discriminatorValue !== null) {
2586 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2587 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2588 21
                : $discriminatorValue;
2589
        }
2590
2591 154
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2592
2593 154 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...
2594 14
            $discriminatorValue = $data[$class->discriminatorField];
2595 154
        } elseif ($class->defaultDiscriminatorValue !== null) {
2596 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2597 1
        }
2598
2599 154
        if ($discriminatorValue !== null) {
2600 15
            return isset($class->discriminatorMap[$discriminatorValue])
2601 15
                ? $class->discriminatorMap[$discriminatorValue]
2602 15
                : $discriminatorValue;
2603
        }
2604
2605 139
        return $mapping['targetDocument'];
2606
    }
2607
2608
    /**
2609
     * INTERNAL:
2610
     * Creates a document. Used for reconstitution of documents during hydration.
2611
     *
2612
     * @ignore
2613
     * @param string $className The name of the document class.
2614
     * @param array $data The data for the document.
2615
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2616
     * @param object The document to be hydrated into in case of creation
2617
     * @return object The document instance.
2618
     * @internal Highly performance-sensitive method.
2619
     */
2620 350
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2621
    {
2622 350
        $class = $this->dm->getClassMetadata($className);
2623
2624
        // @TODO figure out how to remove this
2625 350
        $discriminatorValue = null;
2626 350 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...
2627 19
            $discriminatorValue = $data[$class->discriminatorField];
2628 350
        } elseif (isset($class->defaultDiscriminatorValue)) {
2629 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2630 2
        }
2631
2632 350
        if ($discriminatorValue !== null) {
2633 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2634 20
                ? $class->discriminatorMap[$discriminatorValue]
2635 20
                : $discriminatorValue;
2636
2637 20
            $class = $this->dm->getClassMetadata($className);
2638
2639 20
            unset($data[$class->discriminatorField]);
2640 20
        }
2641
2642 350
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2643 350
        $serializedId = serialize($id);
2644
2645 350
        if (isset($this->identityMap[$class->name][$serializedId])) {
2646 84
            $document = $this->identityMap[$class->name][$serializedId];
2647 84
            $oid = spl_object_hash($document);
2648 84
            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...
2649 9
                $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...
2650 9
                $overrideLocalValues = true;
2651 9
                if ($document instanceof NotifyPropertyChanged) {
2652
                    $document->addPropertyChangedListener($this);
2653
                }
2654 9
            } else {
2655 80
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2656
            }
2657 84
            if ($overrideLocalValues) {
2658 44
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2659 44
                $this->originalDocumentData[$oid] = $data;
2660 44
            }
2661 84
        } else {
2662 324
            if ($document === null) {
2663 324
                $document = $class->newInstance();
2664 324
            }
2665 324
            $this->registerManaged($document, $id, $data);
2666 324
            $oid = spl_object_hash($document);
2667 324
            $this->documentStates[$oid] = self::STATE_MANAGED;
2668 324
            $this->identityMap[$class->name][$serializedId] = $document;
2669 324
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2670 324
            $this->originalDocumentData[$oid] = $data;
2671
        }
2672 350
        return $document;
2673
    }
2674
2675
    /**
2676
     * Initializes (loads) an uninitialized persistent collection of a document.
2677
     *
2678
     * @param PersistentCollection $collection The collection to initialize.
2679
     */
2680 123
    public function loadCollection(PersistentCollection $collection)
2681
    {
2682 123
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2683 123
    }
2684
2685
    /**
2686
     * Gets the identity map of the UnitOfWork.
2687
     *
2688
     * @return array
2689
     */
2690
    public function getIdentityMap()
2691
    {
2692
        return $this->identityMap;
2693
    }
2694
2695
    /**
2696
     * Gets the original data of a document. The original data is the data that was
2697
     * present at the time the document was reconstituted from the database.
2698
     *
2699
     * @param object $document
2700
     * @return array
2701
     */
2702 1
    public function getOriginalDocumentData($document)
2703
    {
2704 1
        $oid = spl_object_hash($document);
2705 1
        if (isset($this->originalDocumentData[$oid])) {
2706 1
            return $this->originalDocumentData[$oid];
2707
        }
2708
        return array();
2709
    }
2710
2711
    /**
2712
     * @ignore
2713
     */
2714 42
    public function setOriginalDocumentData($document, array $data)
2715
    {
2716 42
        $oid = spl_object_hash($document);
2717 42
        $this->originalDocumentData[$oid] = $data;
2718 42
        unset($this->documentChangeSets[$oid]);
2719 42
    }
2720
2721
    /**
2722
     * INTERNAL:
2723
     * Sets a property value of the original data array of a document.
2724
     *
2725
     * @ignore
2726
     * @param string $oid
2727
     * @param string $property
2728
     * @param mixed $value
2729
     */
2730 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2731
    {
2732 3
        $this->originalDocumentData[$oid][$property] = $value;
2733 3
    }
2734
2735
    /**
2736
     * Gets the identifier of a document.
2737
     *
2738
     * @param object $document
2739
     * @return mixed The identifier value
2740
     */
2741 198
    public function getDocumentIdentifier($document)
2742
    {
2743 198
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2744 198
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2745
    }
2746
2747
    /**
2748
     * Checks whether the UnitOfWork has any pending insertions.
2749
     *
2750
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2751
     */
2752
    public function hasPendingInsertions()
2753
    {
2754
        return ! empty($this->documentInsertions);
2755
    }
2756
2757
    /**
2758
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2759
     * number of documents in the identity map.
2760
     *
2761
     * @return integer
2762
     */
2763 2
    public function size()
2764
    {
2765 2
        $count = 0;
2766 2
        foreach ($this->identityMap as $documentSet) {
2767 2
            $count += count($documentSet);
2768 2
        }
2769 2
        return $count;
2770
    }
2771
2772
    /**
2773
     * INTERNAL:
2774
     * Registers a document as managed.
2775
     *
2776
     * TODO: This method assumes that $id is a valid PHP identifier for the
2777
     * document class. If the class expects its database identifier to be a
2778
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2779
     * document identifiers map will become inconsistent with the identity map.
2780
     * In the future, we may want to round-trip $id through a PHP and database
2781
     * conversion and throw an exception if it's inconsistent.
2782
     *
2783
     * @param object $document The document.
2784
     * @param array $id The identifier values.
2785
     * @param array $data The original document data.
2786
     */
2787 345
    public function registerManaged($document, $id, array $data)
2788
    {
2789 345
        $oid = spl_object_hash($document);
2790 345
        $class = $this->dm->getClassMetadata(get_class($document));
2791
2792 345
        if ( ! $class->identifier || $id === null) {
2793 84
            $this->documentIdentifiers[$oid] = $oid;
2794 84
        } else {
2795 340
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2796
        }
2797
2798 345
        $this->documentStates[$oid] = self::STATE_MANAGED;
2799 345
        $this->originalDocumentData[$oid] = $data;
2800 345
        $this->addToIdentityMap($document);
2801 345
    }
2802
2803
    /**
2804
     * INTERNAL:
2805
     * Clears the property changeset of the document with the given OID.
2806
     *
2807
     * @param string $oid The document's OID.
2808
     */
2809 1
    public function clearDocumentChangeSet($oid)
2810
    {
2811 1
        $this->documentChangeSets[$oid] = array();
2812 1
    }
2813
2814
    /* PropertyChangedListener implementation */
2815
2816
    /**
2817
     * Notifies this UnitOfWork of a property change in a document.
2818
     *
2819
     * @param object $document The document that owns the property.
2820
     * @param string $propertyName The name of the property that changed.
2821
     * @param mixed $oldValue The old value of the property.
2822
     * @param mixed $newValue The new value of the property.
2823
     */
2824 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2825
    {
2826 2
        $oid = spl_object_hash($document);
2827 2
        $class = $this->dm->getClassMetadata(get_class($document));
2828
2829 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2830 1
            return; // ignore non-persistent fields
2831
        }
2832
2833
        // Update changeset and mark document for synchronization
2834 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2835 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2836 2
            $this->scheduleForDirtyCheck($document);
2837 2
        }
2838 2
    }
2839
2840
    /**
2841
     * Gets the currently scheduled document insertions in this UnitOfWork.
2842
     *
2843
     * @return array
2844
     */
2845 5
    public function getScheduledDocumentInsertions()
2846
    {
2847 5
        return $this->documentInsertions;
2848
    }
2849
2850
    /**
2851
     * Gets the currently scheduled document upserts in this UnitOfWork.
2852
     *
2853
     * @return array
2854
     */
2855 3
    public function getScheduledDocumentUpserts()
2856
    {
2857 3
        return $this->documentUpserts;
2858
    }
2859
2860
    /**
2861
     * Gets the currently scheduled document updates in this UnitOfWork.
2862
     *
2863
     * @return array
2864
     */
2865 3
    public function getScheduledDocumentUpdates()
2866
    {
2867 3
        return $this->documentUpdates;
2868
    }
2869
2870
    /**
2871
     * Gets the currently scheduled document deletions in this UnitOfWork.
2872
     *
2873
     * @return array
2874
     */
2875
    public function getScheduledDocumentDeletions()
2876
    {
2877
        return $this->documentDeletions;
2878
    }
2879
2880
    /**
2881
     * Get the currently scheduled complete collection deletions
2882
     *
2883
     * @return array
2884
     */
2885
    public function getScheduledCollectionDeletions()
2886
    {
2887
        return $this->collectionDeletions;
2888
    }
2889
2890
    /**
2891
     * Gets the currently scheduled collection inserts, updates and deletes.
2892
     *
2893
     * @return array
2894
     */
2895
    public function getScheduledCollectionUpdates()
2896
    {
2897
        return $this->collectionUpdates;
2898
    }
2899
2900
    /**
2901
     * Helper method to initialize a lazy loading proxy or persistent collection.
2902
     *
2903
     * @param object
2904
     * @return void
2905
     */
2906
    public function initializeObject($obj)
2907
    {
2908
        if ($obj instanceof Proxy) {
2909
            $obj->__load();
2910
        } elseif ($obj instanceof PersistentCollection) {
2911
            $obj->initialize();
2912
        }
2913
    }
2914
2915 1
    private function objToStr($obj)
2916
    {
2917 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2918
    }
2919
}
2920