Completed
Pull Request — master (#1419)
by Robert
39:37 queued 36:54
created

UnitOfWork::getClassNameForAssociation()   D

Complexity

Conditions 10
Paths 66

Size

Total Lines 33
Code Lines 21

Duplication

Lines 5
Ratio 15.15 %

Code Coverage

Tests 21
CRAP Score 10.0658

Importance

Changes 0
Metric Value
dl 5
loc 33
ccs 21
cts 23
cp 0.913
rs 4.8196
c 0
b 0
f 0
cc 10
eloc 21
nc 66
nop 2
crap 10.0658

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

Loading history...
394 24
            $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...
395 24
            $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...
396 24
            $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...
397 600
        ) {
398 24
            return; // Nothing to do.
399
        }
400
401 597
        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...
402 47
            foreach ($this->orphanRemovals as $removal) {
403 47
                $this->remove($removal);
404 47
            }
405 47
        }
406
407
        // Raise onFlush
408 597
        if ($this->evm->hasListeners(Events::onFlush)) {
409 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
410 7
        }
411
412 597
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
413 83
            list($class, $documents) = $classAndDocuments;
414 83
            $this->executeUpserts($class, $documents, $options);
415 597
        }
416
417 597
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
418 525
            list($class, $documents) = $classAndDocuments;
419 525
            $this->executeInserts($class, $documents, $options);
420 596
        }
421
422 596
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
423 231
            list($class, $documents) = $classAndDocuments;
424 231
            $this->executeUpdates($class, $documents, $options);
425 596
        }
426
427 596
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
428 69
            list($class, $documents) = $classAndDocuments;
429 69
            $this->executeDeletions($class, $documents, $options);
430 596
        }
431
432
        // Raise postFlush
433 596
        if ($this->evm->hasListeners(Events::postFlush)) {
434
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
435
        }
436
437
        // Clear up
438 596
        $this->documentInsertions =
439 596
        $this->documentUpserts =
440 596
        $this->documentUpdates =
441 596
        $this->documentDeletions =
442 596
        $this->documentChangeSets =
443 596
        $this->collectionUpdates =
444 596
        $this->collectionDeletions =
445 596
        $this->visitedCollections =
446 596
        $this->scheduledForDirtyCheck =
447 596
        $this->orphanRemovals =
448 596
        $this->hasScheduledCollections = array();
449 596
    }
450
451
    /**
452
     * Groups a list of scheduled documents by their class.
453
     *
454
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
455
     * @param bool $includeEmbedded
456
     * @return array Tuples of ClassMetadata and a corresponding array of objects
457
     */
458 597
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
459
    {
460 597
        if (empty($documents)) {
461 597
            return array();
462
        }
463 596
        $divided = array();
464 596
        $embeds = array();
465 596
        foreach ($documents as $oid => $d) {
466 596
            $className = get_class($d);
467 596
            if (isset($embeds[$className])) {
468 72
                continue;
469
            }
470 596
            if (isset($divided[$className])) {
471 146
                $divided[$className][1][$oid] = $d;
472 146
                continue;
473
            }
474 596
            $class = $this->dm->getClassMetadata($className);
475 596
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
476 175
                $embeds[$className] = true;
477 175
                continue;
478
            }
479 596
            if (empty($divided[$class->name])) {
480 596
                $divided[$class->name] = array($class, array($oid => $d));
481 596
            } else {
482 4
                $divided[$class->name][1][$oid] = $d;
483
            }
484 596
        }
485 596
        return $divided;
486
    }
487
488
    /**
489
     * Compute changesets of all documents scheduled for insertion.
490
     *
491
     * Embedded documents will not be processed.
492
     */
493 604 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...
494
    {
495 604
        foreach ($this->documentInsertions as $document) {
496 533
            $class = $this->dm->getClassMetadata(get_class($document));
497 533
            if ( ! $class->isEmbeddedDocument) {
498 530
                $this->computeChangeSet($class, $document);
499 529
            }
500 603
        }
501 603
    }
502
503
    /**
504
     * Compute changesets of all documents scheduled for upsert.
505
     *
506
     * Embedded documents will not be processed.
507
     */
508 603 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...
509
    {
510 603
        foreach ($this->documentUpserts as $document) {
511 82
            $class = $this->dm->getClassMetadata(get_class($document));
512 82
            if ( ! $class->isEmbeddedDocument) {
513 82
                $this->computeChangeSet($class, $document);
514 82
            }
515 603
        }
516 603
    }
517
518
    /**
519
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
520
     *
521
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
522
     * 2. Proxies are skipped.
523
     * 3. Only if document is properly managed.
524
     *
525
     * @param  object $document
526
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
527
     * @return void
528
     */
529 13
    private function computeSingleDocumentChangeSet($document)
530
    {
531 13
        $state = $this->getDocumentState($document);
532
533 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
534 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
535
        }
536
537 12
        $class = $this->dm->getClassMetadata(get_class($document));
538
539 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
540 9
            $this->persist($document);
541 9
        }
542
543
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
544 12
        $this->computeScheduleInsertsChangeSets();
545 12
        $this->computeScheduleUpsertsChangeSets();
546
547
        // Ignore uninitialized proxy objects
548 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...
549
            return;
550
        }
551
552
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
553 12
        $oid = spl_object_hash($document);
554
555 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...
556 12
            && ! isset($this->documentUpserts[$oid])
557 12
            && ! isset($this->documentDeletions[$oid])
558 12
            && isset($this->documentStates[$oid])
559 12
        ) {
560 8
            $this->computeChangeSet($class, $document);
561 8
        }
562 12
    }
563
564
    /**
565
     * Gets the changeset for a document.
566
     *
567
     * @param object $document
568
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
569
     */
570 597
    public function getDocumentChangeSet($document)
571
    {
572 597
        $oid = spl_object_hash($document);
573 597
        if (isset($this->documentChangeSets[$oid])) {
574 594
            return $this->documentChangeSets[$oid];
575
        }
576 61
        return array();
577
    }
578
579
    /**
580
     * INTERNAL:
581
     * Sets the changeset for a document.
582
     *
583
     * @param object $document
584
     * @param array $changeset
585
     */
586 1
    public function setDocumentChangeSet($document, $changeset)
587
    {
588 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
589 1
    }
590
591
    /**
592
     * Get a documents actual data, flattening all the objects to arrays.
593
     *
594
     * @param object $document
595
     * @return array
596
     */
597 604
    public function getDocumentActualData($document)
598
    {
599 604
        $class = $this->dm->getClassMetadata(get_class($document));
600 604
        $actualData = array();
601 604
        foreach ($class->reflFields as $name => $refProp) {
602 604
            $mapping = $class->fieldMappings[$name];
603
            // skip not saved fields
604 604
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
605 51
                continue;
606
            }
607 604
            $value = $refProp->getValue($document);
608 604
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
609 6
                $value = new GridFSFile($value);
610 6
                $class->reflFields[$name]->setValue($document, $value);
611 6
                $actualData[$name] = $value;
612 604
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
613 604
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
614
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
615 387
                if ( ! $value instanceof Collection) {
616 126
                    $value = new ArrayCollection($value);
617 126
                }
618
619
                // Inject PersistentCollection
620 387
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
621 387
                $coll->setOwner($document, $mapping);
622 387
                $coll->setDirty( ! $value->isEmpty());
623 387
                $class->reflFields[$name]->setValue($document, $coll);
624 387
                $actualData[$name] = $coll;
625 387
            } else {
626 604
                $actualData[$name] = $value;
627
            }
628 604
        }
629 604
        return $actualData;
630
    }
631
632
    /**
633
     * Computes the changes that happened to a single document.
634
     *
635
     * Modifies/populates the following properties:
636
     *
637
     * {@link originalDocumentData}
638
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
639
     * then it was not fetched from the database and therefore we have no original
640
     * document data yet. All of the current document data is stored as the original document data.
641
     *
642
     * {@link documentChangeSets}
643
     * The changes detected on all properties of the document are stored there.
644
     * A change is a tuple array where the first entry is the old value and the second
645
     * entry is the new value of the property. Changesets are used by persisters
646
     * to INSERT/UPDATE the persistent document state.
647
     *
648
     * {@link documentUpdates}
649
     * If the document is already fully MANAGED (has been fetched from the database before)
650
     * and any changes to its properties are detected, then a reference to the document is stored
651
     * there to mark it for an update.
652
     *
653
     * @param ClassMetadata $class The class descriptor of the document.
654
     * @param object $document The document for which to compute the changes.
655
     */
656 601
    public function computeChangeSet(ClassMetadata $class, $document)
657
    {
658 601
        if ( ! $class->isInheritanceTypeNone()) {
659 180
            $class = $this->dm->getClassMetadata(get_class($document));
660 180
        }
661
662
        // Fire PreFlush lifecycle callbacks
663 601 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...
664 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
665 11
        }
666
667 601
        $this->computeOrRecomputeChangeSet($class, $document);
668 600
    }
669
670
    /**
671
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
672
     *
673
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
674
     * @param object $document
675
     * @param boolean $recompute
676
     */
677 601
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
678
    {
679 601
        $oid = spl_object_hash($document);
680 601
        $actualData = $this->getDocumentActualData($document);
681 601
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
682 601
        if ($isNewDocument) {
683
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
684
            // These result in an INSERT.
685 601
            $this->originalDocumentData[$oid] = $actualData;
686 601
            $changeSet = array();
687 601
            foreach ($actualData as $propName => $actualValue) {
688
                /* At this PersistentCollection shouldn't be here, probably it
689
                 * was cloned and its ownership must be fixed
690
                 */
691 601
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
692
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
693
                    $actualValue = $actualData[$propName];
694
                }
695
                // ignore inverse side of reference relationship
696 601 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...
697 184
                    continue;
698
                }
699 601
                $changeSet[$propName] = array(null, $actualValue);
700 601
            }
701 601
            $this->documentChangeSets[$oid] = $changeSet;
702 601
        } else {
703
            // Document is "fully" MANAGED: it was already fully persisted before
704
            // and we have a copy of the original data
705 291
            $originalData = $this->originalDocumentData[$oid];
706 291
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
707 291
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
708 2
                $changeSet = $this->documentChangeSets[$oid];
709 2
            } else {
710 291
                $changeSet = array();
711
            }
712
713 291
            foreach ($actualData as $propName => $actualValue) {
714
                // skip not saved fields
715 291
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
716
                    continue;
717
                }
718
719 291
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
720
721
                // skip if value has not changed
722 291
                if ($orgValue === $actualValue) {
723 290
                    if ($actualValue instanceof PersistentCollectionInterface) {
724 202
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
725
                            // consider dirty collections as changed as well
726 178
                            continue;
727
                        }
728 290
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
729
                        // but consider dirty GridFSFile instances as changed
730 290
                        continue;
731
                    }
732 100
                }
733
734
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
735 250
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
736 11
                    if ($orgValue !== null) {
737 6
                        $this->scheduleOrphanRemoval($orgValue);
738 6
                    }
739
740 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
741 11
                    continue;
742
                }
743
744
                // if owning side of reference-one relationship
745 243
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
746 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
747 1
                        $this->scheduleOrphanRemoval($orgValue);
748 1
                    }
749
750 13
                    $changeSet[$propName] = array($orgValue, $actualValue);
751 13
                    continue;
752
                }
753
754 236
                if ($isChangeTrackingNotify) {
755 3
                    continue;
756
                }
757
758
                // ignore inverse side of reference relationship
759 234 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...
760 6
                    continue;
761
                }
762
763
                // Persistent collection was exchanged with the "originally"
764
                // created one. This can only mean it was cloned and replaced
765
                // on another document.
766 232
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
767 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
768 6
                }
769
770
                // if embed-many or reference-many relationship
771 232
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
772 116
                    $changeSet[$propName] = array($orgValue, $actualValue);
773
                    /* If original collection was exchanged with a non-empty value
774
                     * and $set will be issued, there is no need to $unset it first
775
                     */
776 116
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
777 28
                        continue;
778
                    }
779 96
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
780 17
                        $this->scheduleCollectionDeletion($orgValue);
781 17
                    }
782 96
                    continue;
783
                }
784
785
                // skip equivalent date values
786 153
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
787 36
                    $dateType = Type::getType('date');
788 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
789 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
790
791 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
792 29
                        continue;
793
                    }
794 10
                }
795
796
                // regular field
797 137
                $changeSet[$propName] = array($orgValue, $actualValue);
798 291
            }
799 291
            if ($changeSet) {
800 239
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
801 239
                    ? $changeSet + $this->documentChangeSets[$oid]
802 21
                    : $changeSet;
803
804 239
                $this->originalDocumentData[$oid] = $actualData;
805 239
                $this->scheduleForUpdate($document);
806 239
            }
807
        }
808
809
        // Look for changes in associations of the document
810 601
        $associationMappings = array_filter(
811 601
            $class->associationMappings,
812
            function ($assoc) { return empty($assoc['notSaved']); }
813 601
        );
814
815 601
        foreach ($associationMappings as $mapping) {
816 456
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
817
818 456
            if ($value === null) {
819 309
                continue;
820
            }
821
822 443
            $this->computeAssociationChanges($document, $mapping, $value);
823
824 442
            if (isset($mapping['reference'])) {
825 337
                continue;
826
            }
827
828 344
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
829
830 344
            foreach ($values as $obj) {
831 179
                $oid2 = spl_object_hash($obj);
832
833 179
                if (isset($this->documentChangeSets[$oid2])) {
834 177
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
835
836 177
                    if ( ! $isNewDocument) {
837 78
                        $this->scheduleForUpdate($document);
838 78
                    }
839
840 177
                    break;
841
                }
842 344
            }
843 600
        }
844 600
    }
845
846
    /**
847
     * Computes all the changes that have been done to documents and collections
848
     * since the last commit and stores these changes in the _documentChangeSet map
849
     * temporarily for access by the persisters, until the UoW commit is finished.
850
     */
851 599
    public function computeChangeSets()
852
    {
853 599
        $this->computeScheduleInsertsChangeSets();
854 598
        $this->computeScheduleUpsertsChangeSets();
855
856
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
857 598
        foreach ($this->identityMap as $className => $documents) {
858 598
            $class = $this->dm->getClassMetadata($className);
859 598
            if ($class->isEmbeddedDocument) {
860
                /* we do not want to compute changes to embedded documents up front
861
                 * in case embedded document was replaced and its changeset
862
                 * would corrupt data. Embedded documents' change set will
863
                 * be calculated by reachability from owning document.
864
                 */
865 167
                continue;
866
            }
867
868
            // If change tracking is explicit or happens through notification, then only compute
869
            // changes on document of that type that are explicitly marked for synchronization.
870 598
            switch (true) {
871 598
                case ($class->isChangeTrackingDeferredImplicit()):
872 597
                    $documentsToProcess = $documents;
873 597
                    break;
874
875 4
                case (isset($this->scheduledForDirtyCheck[$className])):
876 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
877 3
                    break;
878
879 4
                default:
880 4
                    $documentsToProcess = array();
881
882 4
            }
883
884 598
            foreach ($documentsToProcess as $document) {
885
                // Ignore uninitialized proxy objects
886 594
                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...
887 10
                    continue;
888
                }
889
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
890 594
                $oid = spl_object_hash($document);
891 594 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...
892 594
                    && ! isset($this->documentUpserts[$oid])
893 594
                    && ! isset($this->documentDeletions[$oid])
894 594
                    && isset($this->documentStates[$oid])
895 594
                ) {
896 276
                    $this->computeChangeSet($class, $document);
897 276
                }
898 598
            }
899 598
        }
900 598
    }
901
902
    /**
903
     * Computes the changes of an association.
904
     *
905
     * @param object $parentDocument
906
     * @param array $assoc
907
     * @param mixed $value The value of the association.
908
     * @throws \InvalidArgumentException
909
     */
910 443
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
911
    {
912 443
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
913 443
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
914 443
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
915
916 443
        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...
917 8
            return;
918
        }
919
920 442
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
921 241
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
922 237
                $this->scheduleCollectionUpdate($value);
923 237
            }
924 241
            $topmostOwner = $this->getOwningDocument($value->getOwner());
925 241
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
926 241
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
927 140
                $value->initialize();
928 140
                foreach ($value->getDeletedDocuments() as $orphan) {
929 22
                    $this->scheduleOrphanRemoval($orphan);
930 140
                }
931 140
            }
932 241
        }
933
934
        // Look through the documents, and in any of their associations,
935
        // for transient (new) documents, recursively. ("Persistence by reachability")
936
        // Unwrap. Uninitialized collections will simply be empty.
937 442
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
938
939 442
        $count = 0;
940 442
        foreach ($unwrappedValue as $key => $entry) {
941 347
            if ( ! is_object($entry)) {
942 1
                throw new \InvalidArgumentException(
943 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
944 1
                );
945
            }
946
947 346
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
948
949 346
            $state = $this->getDocumentState($entry, self::STATE_NEW);
950
951
            // Handle "set" strategy for multi-level hierarchy
952 346
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
953 346
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
954
955 346
            $count++;
956
957
            switch ($state) {
958 346
                case self::STATE_NEW:
959 59
                    if ( ! $assoc['isCascadePersist']) {
960
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
961
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
962
                            . ' Explicitly persist the new document or configure cascading persist operations'
963
                            . ' on the relationship.');
964
                    }
965
966 59
                    $this->persistNew($targetClass, $entry);
967 59
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
968 59
                    $this->computeChangeSet($targetClass, $entry);
969 59
                    break;
970
971 340
                case self::STATE_MANAGED:
972 340
                    if ($targetClass->isEmbeddedDocument) {
973 169
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
974 169
                        if ($knownParent && $knownParent !== $parentDocument) {
975 6
                            $entry = clone $entry;
976 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
977 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
978 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
979 3
                            } else {
980
                                // must use unwrapped value to not trigger orphan removal
981 6
                                $unwrappedValue[$key] = $entry;
982
                            }
983 6
                            $this->persistNew($targetClass, $entry);
984 6
                        }
985 169
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
986 169
                        $this->computeChangeSet($targetClass, $entry);
987 169
                    }
988 340
                    break;
989
990 1
                case self::STATE_REMOVED:
991
                    // Consume the $value as array (it's either an array or an ArrayAccess)
992
                    // and remove the element from Collection.
993 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
994
                        unset($value[$key]);
995
                    }
996 1
                    break;
997
998
                case self::STATE_DETACHED:
999
                    // Can actually not happen right now as we assume STATE_NEW,
1000
                    // so the exception will be raised from the DBAL layer (constraint violation).
1001
                    throw new \InvalidArgumentException('A detached document was found through a '
1002
                        . 'relationship during cascading a persist operation.');
1003
1004
                default:
1005
                    // MANAGED associated documents are already taken into account
1006
                    // during changeset calculation anyway, since they are in the identity map.
1007
1008
            }
1009 441
        }
1010 441
    }
1011
1012
    /**
1013
     * INTERNAL:
1014
     * Computes the changeset of an individual document, independently of the
1015
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1016
     *
1017
     * The passed document must be a managed document. If the document already has a change set
1018
     * because this method is invoked during a commit cycle then the change sets are added.
1019
     * whereby changes detected in this method prevail.
1020
     *
1021
     * @ignore
1022
     * @param ClassMetadata $class The class descriptor of the document.
1023
     * @param object $document The document for which to (re)calculate the change set.
1024
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1025
     */
1026 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1027
    {
1028
        // Ignore uninitialized proxy objects
1029 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1030 1
            return;
1031
        }
1032
1033 19
        $oid = spl_object_hash($document);
1034
1035 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1036
            throw new \InvalidArgumentException('Document must be managed.');
1037
        }
1038
1039 19
        if ( ! $class->isInheritanceTypeNone()) {
1040 2
            $class = $this->dm->getClassMetadata(get_class($document));
1041 2
        }
1042
1043 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1044 19
    }
1045
1046
    /**
1047
     * @param ClassMetadata $class
1048
     * @param object $document
1049
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1050
     */
1051 626
    private function persistNew(ClassMetadata $class, $document)
1052
    {
1053 626
        $this->lifecycleEventManager->prePersist($class, $document);
1054 626
        $oid = spl_object_hash($document);
1055 626
        $upsert = false;
1056 626
        if ($class->identifier) {
1057 626
            $idValue = $class->getIdentifierValue($document);
1058 626
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1059
1060 626
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1061 3
                throw new \InvalidArgumentException(sprintf(
1062 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1063 3
                    get_class($document)
1064 3
                ));
1065
            }
1066
1067 625
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1068 1
                throw new \InvalidArgumentException(sprintf(
1069 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1070 1
                    get_class($document)
1071 1
                ));
1072
            }
1073
1074 624
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1075 548
                $idValue = $class->idGenerator->generate($this->dm, $document);
1076 548
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1077 548
                $class->setIdentifierValue($document, $idValue);
1078 548
            }
1079
1080 624
            $this->documentIdentifiers[$oid] = $idValue;
1081 624
        } else {
1082
            // this is for embedded documents without identifiers
1083 152
            $this->documentIdentifiers[$oid] = $oid;
1084
        }
1085
1086 624
        $this->documentStates[$oid] = self::STATE_MANAGED;
1087
1088 624
        if ($upsert) {
1089 86
            $this->scheduleForUpsert($class, $document);
1090 86
        } else {
1091 553
            $this->scheduleForInsert($class, $document);
1092
        }
1093 624
    }
1094
1095
    /**
1096
     * Executes all document insertions for documents of the specified type.
1097
     *
1098
     * @param ClassMetadata $class
1099
     * @param array $documents Array of documents to insert
1100
     * @param array $options Array of options to be used with batchInsert()
1101
     */
1102 525 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...
1103
    {
1104 525
        $persister = $this->getDocumentPersister($class->name);
1105
1106 525
        foreach ($documents as $oid => $document) {
1107 525
            $persister->addInsert($document);
1108 525
            unset($this->documentInsertions[$oid]);
1109 525
        }
1110
1111 525
        $persister->executeInserts($options);
1112
1113 524
        foreach ($documents as $document) {
1114 524
            $this->lifecycleEventManager->postPersist($class, $document);
1115 524
        }
1116 524
    }
1117
1118
    /**
1119
     * Executes all document upserts for documents of the specified type.
1120
     *
1121
     * @param ClassMetadata $class
1122
     * @param array $documents Array of documents to upsert
1123
     * @param array $options Array of options to be used with batchInsert()
1124
     */
1125 83 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...
1126
    {
1127 83
        $persister = $this->getDocumentPersister($class->name);
1128
1129
1130 83
        foreach ($documents as $oid => $document) {
1131 83
            $persister->addUpsert($document);
1132 83
            unset($this->documentUpserts[$oid]);
1133 83
        }
1134
1135 83
        $persister->executeUpserts($options);
1136
1137 83
        foreach ($documents as $document) {
1138 83
            $this->lifecycleEventManager->postPersist($class, $document);
1139 83
        }
1140 83
    }
1141
1142
    /**
1143
     * Executes all document updates for documents of the specified type.
1144
     *
1145
     * @param Mapping\ClassMetadata $class
1146
     * @param array $documents Array of documents to update
1147
     * @param array $options Array of options to be used with update()
1148
     */
1149 231
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1150
    {
1151 231
        $className = $class->name;
1152 231
        $persister = $this->getDocumentPersister($className);
1153
1154 231
        foreach ($documents as $oid => $document) {
1155 231
            $this->lifecycleEventManager->preUpdate($class, $document);
1156
1157 231
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1158 229
                $persister->update($document, $options);
1159 223
            }
1160
1161 225
            unset($this->documentUpdates[$oid]);
1162
1163 225
            $this->lifecycleEventManager->postUpdate($class, $document);
1164 225
        }
1165 224
    }
1166
1167
    /**
1168
     * Executes all document deletions for documents of the specified type.
1169
     *
1170
     * @param ClassMetadata $class
1171
     * @param array $documents Array of documents to delete
1172
     * @param array $options Array of options to be used with remove()
1173
     */
1174 69
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1175
    {
1176 69
        $persister = $this->getDocumentPersister($class->name);
1177
1178 69
        foreach ($documents as $oid => $document) {
1179 69
            if ( ! $class->isEmbeddedDocument) {
1180 33
                $persister->delete($document, $options);
1181 31
            }
1182
            unset(
1183 67
                $this->documentDeletions[$oid],
1184 67
                $this->documentIdentifiers[$oid],
1185 67
                $this->originalDocumentData[$oid]
1186
            );
1187
1188
            // Clear snapshot information for any referenced PersistentCollection
1189
            // http://www.doctrine-project.org/jira/browse/MODM-95
1190 67
            foreach ($class->associationMappings as $fieldMapping) {
1191 43
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1192 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1193 27
                    if ($value instanceof PersistentCollectionInterface) {
1194 23
                        $value->clearSnapshot();
1195 23
                    }
1196 27
                }
1197 67
            }
1198
1199
            // Document with this $oid after deletion treated as NEW, even if the $oid
1200
            // is obtained by a new document because the old one went out of scope.
1201 67
            $this->documentStates[$oid] = self::STATE_NEW;
1202
1203 67
            $this->lifecycleEventManager->postRemove($class, $document);
1204 67
        }
1205 67
    }
1206
1207
    /**
1208
     * Schedules a document for insertion into the database.
1209
     * If the document already has an identifier, it will be added to the
1210
     * identity map.
1211
     *
1212
     * @param ClassMetadata $class
1213
     * @param object $document The document to schedule for insertion.
1214
     * @throws \InvalidArgumentException
1215
     */
1216 556
    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...
1217
    {
1218 556
        $oid = spl_object_hash($document);
1219
1220 556
        if (isset($this->documentUpdates[$oid])) {
1221
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1222
        }
1223 556
        if (isset($this->documentDeletions[$oid])) {
1224
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1225
        }
1226 556
        if (isset($this->documentInsertions[$oid])) {
1227
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1228
        }
1229
1230 556
        $this->documentInsertions[$oid] = $document;
1231
1232 556
        if (isset($this->documentIdentifiers[$oid])) {
1233 553
            $this->addToIdentityMap($document);
1234 553
        }
1235 556
    }
1236
1237
    /**
1238
     * Schedules a document for upsert into the database and adds it to the
1239
     * identity map
1240
     *
1241
     * @param ClassMetadata $class
1242
     * @param object $document The document to schedule for upsert.
1243
     * @throws \InvalidArgumentException
1244
     */
1245 89
    public function scheduleForUpsert(ClassMetadata $class, $document)
1246
    {
1247 89
        $oid = spl_object_hash($document);
1248
1249 89
        if ($class->isEmbeddedDocument) {
1250
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1251
        }
1252 89
        if (isset($this->documentUpdates[$oid])) {
1253
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1254
        }
1255 89
        if (isset($this->documentDeletions[$oid])) {
1256
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1257
        }
1258 89
        if (isset($this->documentUpserts[$oid])) {
1259
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1260
        }
1261
1262 89
        $this->documentUpserts[$oid] = $document;
1263 89
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1264 89
        $this->addToIdentityMap($document);
1265 89
    }
1266
1267
    /**
1268
     * Checks whether a document is scheduled for insertion.
1269
     *
1270
     * @param object $document
1271
     * @return boolean
1272
     */
1273 103
    public function isScheduledForInsert($document)
1274
    {
1275 103
        return isset($this->documentInsertions[spl_object_hash($document)]);
1276
    }
1277
1278
    /**
1279
     * Checks whether a document is scheduled for upsert.
1280
     *
1281
     * @param object $document
1282
     * @return boolean
1283
     */
1284 5
    public function isScheduledForUpsert($document)
1285
    {
1286 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1287
    }
1288
1289
    /**
1290
     * Schedules a document for being updated.
1291
     *
1292
     * @param object $document The document to schedule for being updated.
1293
     * @throws \InvalidArgumentException
1294
     */
1295 240
    public function scheduleForUpdate($document)
1296
    {
1297 240
        $oid = spl_object_hash($document);
1298 240
        if ( ! isset($this->documentIdentifiers[$oid])) {
1299
            throw new \InvalidArgumentException('Document has no identity.');
1300
        }
1301
1302 240
        if (isset($this->documentDeletions[$oid])) {
1303
            throw new \InvalidArgumentException('Document is removed.');
1304
        }
1305
1306 240
        if ( ! isset($this->documentUpdates[$oid])
1307 240
            && ! isset($this->documentInsertions[$oid])
1308 240
            && ! isset($this->documentUpserts[$oid])) {
1309 236
            $this->documentUpdates[$oid] = $document;
1310 236
        }
1311 240
    }
1312
1313
    /**
1314
     * Checks whether a document is registered as dirty in the unit of work.
1315
     * Note: Is not very useful currently as dirty documents are only registered
1316
     * at commit time.
1317
     *
1318
     * @param object $document
1319
     * @return boolean
1320
     */
1321 22
    public function isScheduledForUpdate($document)
1322
    {
1323 22
        return isset($this->documentUpdates[spl_object_hash($document)]);
1324
    }
1325
1326 1
    public function isScheduledForDirtyCheck($document)
1327
    {
1328 1
        $class = $this->dm->getClassMetadata(get_class($document));
1329 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1330
    }
1331
1332
    /**
1333
     * INTERNAL:
1334
     * Schedules a document for deletion.
1335
     *
1336
     * @param object $document
1337
     */
1338 74
    public function scheduleForDelete($document)
1339
    {
1340 74
        $oid = spl_object_hash($document);
1341
1342 74
        if (isset($this->documentInsertions[$oid])) {
1343 2
            if ($this->isInIdentityMap($document)) {
1344 2
                $this->removeFromIdentityMap($document);
1345 2
            }
1346 2
            unset($this->documentInsertions[$oid]);
1347 2
            return; // document has not been persisted yet, so nothing more to do.
1348
        }
1349
1350 73
        if ( ! $this->isInIdentityMap($document)) {
1351 1
            return; // ignore
1352
        }
1353
1354 72
        $this->removeFromIdentityMap($document);
1355 72
        $this->documentStates[$oid] = self::STATE_REMOVED;
1356
1357 72
        if (isset($this->documentUpdates[$oid])) {
1358
            unset($this->documentUpdates[$oid]);
1359
        }
1360 72
        if ( ! isset($this->documentDeletions[$oid])) {
1361 72
            $this->documentDeletions[$oid] = $document;
1362 72
        }
1363 72
    }
1364
1365
    /**
1366
     * Checks whether a document is registered as removed/deleted with the unit
1367
     * of work.
1368
     *
1369
     * @param object $document
1370
     * @return boolean
1371
     */
1372 8
    public function isScheduledForDelete($document)
1373
    {
1374 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1375
    }
1376
1377
    /**
1378
     * Checks whether a document is scheduled for insertion, update or deletion.
1379
     *
1380
     * @param $document
1381
     * @return boolean
1382
     */
1383 240
    public function isDocumentScheduled($document)
1384
    {
1385 240
        $oid = spl_object_hash($document);
1386 240
        return isset($this->documentInsertions[$oid]) ||
1387 128
            isset($this->documentUpserts[$oid]) ||
1388 119
            isset($this->documentUpdates[$oid]) ||
1389 240
            isset($this->documentDeletions[$oid]);
1390
    }
1391
1392
    /**
1393
     * INTERNAL:
1394
     * Registers a document in the identity map.
1395
     *
1396
     * Note that documents in a hierarchy are registered with the class name of
1397
     * the root document. Identifiers are serialized before being used as array
1398
     * keys to allow differentiation of equal, but not identical, values.
1399
     *
1400
     * @ignore
1401
     * @param object $document  The document to register.
1402
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1403
     *                  the document in question is already managed.
1404
     */
1405 655
    public function addToIdentityMap($document)
1406
    {
1407 655
        $class = $this->dm->getClassMetadata(get_class($document));
1408 655
        $id = $this->getIdForIdentityMap($document);
1409
1410 655
        if (isset($this->identityMap[$class->name][$id])) {
1411 54
            return false;
1412
        }
1413
1414 655
        $this->identityMap[$class->name][$id] = $document;
1415
1416 655
        if ($document instanceof NotifyPropertyChanged &&
1417 655
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1418 4
            $document->addPropertyChangedListener($this);
1419 4
        }
1420
1421 655
        return true;
1422
    }
1423
1424
    /**
1425
     * Gets the state of a document with regard to the current unit of work.
1426
     *
1427
     * @param object   $document
1428
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1429
     *                         This parameter can be set to improve performance of document state detection
1430
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1431
     *                         is either known or does not matter for the caller of the method.
1432
     * @return int The document state.
1433
     */
1434 629
    public function getDocumentState($document, $assume = null)
1435
    {
1436 629
        $oid = spl_object_hash($document);
1437
1438 629
        if (isset($this->documentStates[$oid])) {
1439 382
            return $this->documentStates[$oid];
1440
        }
1441
1442 629
        $class = $this->dm->getClassMetadata(get_class($document));
1443
1444 629
        if ($class->isEmbeddedDocument) {
1445 185
            return self::STATE_NEW;
1446
        }
1447
1448 626
        if ($assume !== null) {
1449 623
            return $assume;
1450
        }
1451
1452
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1453
         * known. Note that you cannot remember the NEW or DETACHED state in
1454
         * _documentStates since the UoW does not hold references to such
1455
         * objects and the object hash can be reused. More generally, because
1456
         * the state may "change" between NEW/DETACHED without the UoW being
1457
         * aware of it.
1458
         */
1459 4
        $id = $class->getIdentifierObject($document);
1460
1461 4
        if ($id === null) {
1462 2
            return self::STATE_NEW;
1463
        }
1464
1465
        // Check for a version field, if available, to avoid a DB lookup.
1466 2
        if ($class->isVersioned) {
1467
            return $class->getFieldValue($document, $class->versionField)
1468
                ? self::STATE_DETACHED
1469
                : self::STATE_NEW;
1470
        }
1471
1472
        // Last try before DB lookup: check the identity map.
1473 2
        if ($this->tryGetById($id, $class)) {
1474 1
            return self::STATE_DETACHED;
1475
        }
1476
1477
        // DB lookup
1478 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1479 1
            return self::STATE_DETACHED;
1480
        }
1481
1482 1
        return self::STATE_NEW;
1483
    }
1484
1485
    /**
1486
     * INTERNAL:
1487
     * Removes a document from the identity map. This effectively detaches the
1488
     * document from the persistence management of Doctrine.
1489
     *
1490
     * @ignore
1491
     * @param object $document
1492
     * @throws \InvalidArgumentException
1493
     * @return boolean
1494
     */
1495 83
    public function removeFromIdentityMap($document)
1496
    {
1497 83
        $oid = spl_object_hash($document);
1498
1499
        // Check if id is registered first
1500 83
        if ( ! isset($this->documentIdentifiers[$oid])) {
1501
            return false;
1502
        }
1503
1504 83
        $class = $this->dm->getClassMetadata(get_class($document));
1505 83
        $id = $this->getIdForIdentityMap($document);
1506
1507 83
        if (isset($this->identityMap[$class->name][$id])) {
1508 83
            unset($this->identityMap[$class->name][$id]);
1509 83
            $this->documentStates[$oid] = self::STATE_DETACHED;
1510 83
            return true;
1511
        }
1512
1513
        return false;
1514
    }
1515
1516
    /**
1517
     * INTERNAL:
1518
     * Gets a document in the identity map by its identifier hash.
1519
     *
1520
     * @ignore
1521
     * @param mixed         $id    Document identifier
1522
     * @param ClassMetadata $class Document class
1523
     * @return object
1524
     * @throws InvalidArgumentException if the class does not have an identifier
1525
     */
1526 34
    public function getById($id, ClassMetadata $class)
1527
    {
1528 34
        if ( ! $class->identifier) {
1529
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1530
        }
1531
1532 34
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1533
1534 34
        return $this->identityMap[$class->name][$serializedId];
1535
    }
1536
1537
    /**
1538
     * INTERNAL:
1539
     * Tries to get a document by its identifier hash. If no document is found
1540
     * for the given hash, FALSE is returned.
1541
     *
1542
     * @ignore
1543
     * @param mixed         $id    Document identifier
1544
     * @param ClassMetadata $class Document class
1545
     * @return mixed The found document or FALSE.
1546
     * @throws InvalidArgumentException if the class does not have an identifier
1547
     */
1548 303
    public function tryGetById($id, ClassMetadata $class)
1549
    {
1550 303
        if ( ! $class->identifier) {
1551
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1552
        }
1553
1554 303
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1555
1556 303
        return isset($this->identityMap[$class->name][$serializedId]) ?
1557 303
            $this->identityMap[$class->name][$serializedId] : false;
1558
    }
1559
1560
    /**
1561
     * Schedules a document for dirty-checking at commit-time.
1562
     *
1563
     * @param object $document The document to schedule for dirty-checking.
1564
     * @todo Rename: scheduleForSynchronization
1565
     */
1566 3
    public function scheduleForDirtyCheck($document)
1567
    {
1568 3
        $class = $this->dm->getClassMetadata(get_class($document));
1569 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1570 3
    }
1571
1572
    /**
1573
     * Checks whether a document is registered in the identity map.
1574
     *
1575
     * @param object $document
1576
     * @return boolean
1577
     */
1578 85
    public function isInIdentityMap($document)
1579
    {
1580 85
        $oid = spl_object_hash($document);
1581
1582 85
        if ( ! isset($this->documentIdentifiers[$oid])) {
1583 6
            return false;
1584
        }
1585
1586 83
        $class = $this->dm->getClassMetadata(get_class($document));
1587 83
        $id = $this->getIdForIdentityMap($document);
1588
1589 83
        return isset($this->identityMap[$class->name][$id]);
1590
    }
1591
1592
    /**
1593
     * @param object $document
1594
     * @return string
1595
     */
1596 655
    private function getIdForIdentityMap($document)
1597
    {
1598 655
        $class = $this->dm->getClassMetadata(get_class($document));
1599
1600 655
        if ( ! $class->identifier) {
1601 155
            $id = spl_object_hash($document);
1602 155
        } else {
1603 654
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1604 654
            $id = serialize($class->getDatabaseIdentifierValue($id));
1605
        }
1606
1607 655
        return $id;
1608
    }
1609
1610
    /**
1611
     * INTERNAL:
1612
     * Checks whether an identifier exists in the identity map.
1613
     *
1614
     * @ignore
1615
     * @param string $id
1616
     * @param string $rootClassName
1617
     * @return boolean
1618
     */
1619
    public function containsId($id, $rootClassName)
1620
    {
1621
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1622
    }
1623
1624
    /**
1625
     * Persists a document as part of the current unit of work.
1626
     *
1627
     * @param object $document The document to persist.
1628
     * @throws MongoDBException If trying to persist MappedSuperclass.
1629
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1630
     */
1631 624
    public function persist($document)
1632
    {
1633 624
        $class = $this->dm->getClassMetadata(get_class($document));
1634 624
        if ($class->isMappedSuperclass) {
1635 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1636
        }
1637 623
        $visited = array();
1638 623
        $this->doPersist($document, $visited);
1639 619
    }
1640
1641
    /**
1642
     * Saves a document as part of the current unit of work.
1643
     * This method is internally called during save() cascades as it tracks
1644
     * the already visited documents to prevent infinite recursions.
1645
     *
1646
     * NOTE: This method always considers documents that are not yet known to
1647
     * this UnitOfWork as NEW.
1648
     *
1649
     * @param object $document The document to persist.
1650
     * @param array $visited The already visited documents.
1651
     * @throws \InvalidArgumentException
1652
     * @throws MongoDBException
1653
     */
1654 623
    private function doPersist($document, array &$visited)
1655
    {
1656 623
        $oid = spl_object_hash($document);
1657 623
        if (isset($visited[$oid])) {
1658 24
            return; // Prevent infinite recursion
1659
        }
1660
1661 623
        $visited[$oid] = $document; // Mark visited
1662
1663 623
        $class = $this->dm->getClassMetadata(get_class($document));
1664
1665 623
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1666
        switch ($documentState) {
1667 623
            case self::STATE_MANAGED:
1668
                // Nothing to do, except if policy is "deferred explicit"
1669 51
                if ($class->isChangeTrackingDeferredExplicit()) {
1670
                    $this->scheduleForDirtyCheck($document);
1671
                }
1672 51
                break;
1673 623
            case self::STATE_NEW:
1674 623
                $this->persistNew($class, $document);
1675 621
                break;
1676
1677 2
            case self::STATE_REMOVED:
1678
                // Document becomes managed again
1679 2
                unset($this->documentDeletions[$oid]);
1680
1681 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1682 2
                break;
1683
1684
            case self::STATE_DETACHED:
1685
                throw new \InvalidArgumentException(
1686
                    'Behavior of persist() for a detached document is not yet defined.');
1687
1688
            default:
1689
                throw MongoDBException::invalidDocumentState($documentState);
1690
        }
1691
1692 621
        $this->cascadePersist($document, $visited);
1693 619
    }
1694
1695
    /**
1696
     * Deletes a document as part of the current unit of work.
1697
     *
1698
     * @param object $document The document to remove.
1699
     */
1700 73
    public function remove($document)
1701
    {
1702 73
        $visited = array();
1703 73
        $this->doRemove($document, $visited);
1704 73
    }
1705
1706
    /**
1707
     * Deletes a document as part of the current unit of work.
1708
     *
1709
     * This method is internally called during delete() cascades as it tracks
1710
     * the already visited documents to prevent infinite recursions.
1711
     *
1712
     * @param object $document The document to delete.
1713
     * @param array $visited The map of the already visited documents.
1714
     * @throws MongoDBException
1715
     */
1716 73
    private function doRemove($document, array &$visited)
1717
    {
1718 73
        $oid = spl_object_hash($document);
1719 73
        if (isset($visited[$oid])) {
1720 1
            return; // Prevent infinite recursion
1721
        }
1722
1723 73
        $visited[$oid] = $document; // mark visited
1724
1725
        /* Cascade first, because scheduleForDelete() removes the entity from
1726
         * the identity map, which can cause problems when a lazy Proxy has to
1727
         * be initialized for the cascade operation.
1728
         */
1729 73
        $this->cascadeRemove($document, $visited);
1730
1731 73
        $class = $this->dm->getClassMetadata(get_class($document));
1732 73
        $documentState = $this->getDocumentState($document);
1733
        switch ($documentState) {
1734 73
            case self::STATE_NEW:
1735 73
            case self::STATE_REMOVED:
1736
                // nothing to do
1737 1
                break;
1738 73
            case self::STATE_MANAGED:
1739 73
                $this->lifecycleEventManager->preRemove($class, $document);
1740 73
                $this->scheduleForDelete($document);
1741 73
                break;
1742
            case self::STATE_DETACHED:
1743
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1744
            default:
1745
                throw MongoDBException::invalidDocumentState($documentState);
1746
        }
1747 73
    }
1748
1749
    /**
1750
     * Merges the state of the given detached document into this UnitOfWork.
1751
     *
1752
     * @param object $document
1753
     * @return object The managed copy of the document.
1754
     */
1755 13
    public function merge($document)
1756
    {
1757 13
        $visited = array();
1758
1759 13
        return $this->doMerge($document, $visited);
1760
    }
1761
1762
    /**
1763
     * Executes a merge operation on a document.
1764
     *
1765
     * @param object      $document
1766
     * @param array       $visited
1767
     * @param object|null $prevManagedCopy
1768
     * @param array|null  $assoc
1769
     *
1770
     * @return object The managed copy of the document.
1771
     *
1772
     * @throws InvalidArgumentException If the entity instance is NEW.
1773
     * @throws LockException If the document uses optimistic locking through a
1774
     *                       version attribute and the version check against the
1775
     *                       managed copy fails.
1776
     */
1777 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1778
    {
1779 13
        $oid = spl_object_hash($document);
1780
1781 13
        if (isset($visited[$oid])) {
1782 1
            return $visited[$oid]; // Prevent infinite recursion
1783
        }
1784
1785 13
        $visited[$oid] = $document; // mark visited
1786
1787 13
        $class = $this->dm->getClassMetadata(get_class($document));
1788
1789
        /* First we assume DETACHED, although it can still be NEW but we can
1790
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1791
         * an identity, we need to fetch it from the DB anyway in order to
1792
         * merge. MANAGED documents are ignored by the merge operation.
1793
         */
1794 13
        $managedCopy = $document;
1795
1796 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1797 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1798
                $document->__load();
1799
            }
1800
1801
            // Try to look the document up in the identity map.
1802 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1803
1804 13
            if ($id === null) {
1805
                // If there is no identifier, it is actually NEW.
1806 5
                $managedCopy = $class->newInstance();
1807 5
                $this->persistNew($class, $managedCopy);
1808 5
            } else {
1809 10
                $managedCopy = $this->tryGetById($id, $class);
1810
1811 10
                if ($managedCopy) {
1812
                    // We have the document in memory already, just make sure it is not removed.
1813 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1814
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1815
                    }
1816 5
                } else {
1817
                    // We need to fetch the managed copy in order to merge.
1818 7
                    $managedCopy = $this->dm->find($class->name, $id);
1819
                }
1820
1821 10
                if ($managedCopy === null) {
1822
                    // If the identifier is ASSIGNED, it is NEW
1823
                    $managedCopy = $class->newInstance();
1824
                    $class->setIdentifierValue($managedCopy, $id);
1825
                    $this->persistNew($class, $managedCopy);
1826
                } else {
1827 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...
1828
                        $managedCopy->__load();
1829
                    }
1830
                }
1831
            }
1832
1833 13
            if ($class->isVersioned) {
1834
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1835
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1836
1837
                // Throw exception if versions don't match
1838
                if ($managedCopyVersion != $documentVersion) {
1839
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1840
                }
1841
            }
1842
1843
            // Merge state of $document into existing (managed) document
1844 13
            foreach ($class->reflClass->getProperties() as $prop) {
1845 13
                $name = $prop->name;
1846 13
                $prop->setAccessible(true);
1847 13
                if ( ! isset($class->associationMappings[$name])) {
1848 13
                    if ( ! $class->isIdentifier($name)) {
1849 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
1850 13
                    }
1851 13
                } else {
1852 13
                    $assoc2 = $class->associationMappings[$name];
1853
1854 13
                    if ($assoc2['type'] === 'one') {
1855 5
                        $other = $prop->getValue($document);
1856
1857 5
                        if ($other === null) {
1858 2
                            $prop->setValue($managedCopy, null);
1859 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...
1860
                            // Do not merge fields marked lazy that have not been fetched
1861 1
                            continue;
1862 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1863
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1864
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1865
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1866
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1867
                                $relatedId = $targetClass->getIdentifierObject($other);
1868
1869
                                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...
1870
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1871
                                } else {
1872
                                    $other = $this
1873
                                        ->dm
1874
                                        ->getProxyFactory()
1875
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1876
                                    $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...
1877
                                }
1878
                            }
1879
1880
                            $prop->setValue($managedCopy, $other);
1881
                        }
1882 4
                    } else {
1883 10
                        $mergeCol = $prop->getValue($document);
1884
1885 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized()) {
1886
                            /* Do not merge fields marked lazy that have not
1887
                             * been fetched. Keep the lazy persistent collection
1888
                             * of the managed copy.
1889
                             */
1890 3
                            continue;
1891
                        }
1892
1893 7
                        $managedCol = $prop->getValue($managedCopy);
1894
1895 7
                        if ( ! $managedCol) {
1896 2
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1897 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1898 2
                            $prop->setValue($managedCopy, $managedCol);
1899 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1900 2
                        }
1901
1902
                        /* Note: do not process association's target documents.
1903
                         * They will be handled during the cascade. Initialize
1904
                         * and, if necessary, clear $managedCol for now.
1905
                         */
1906 7
                        if ($assoc2['isCascadeMerge']) {
1907 7
                            $managedCol->initialize();
1908
1909
                            // If $managedCol differs from the merged collection, clear and set dirty
1910 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1911 2
                                $managedCol->unwrap()->clear();
1912 2
                                $managedCol->setDirty(true);
1913
1914 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1915
                                    $this->scheduleForDirtyCheck($managedCopy);
1916
                                }
1917 2
                            }
1918 7
                        }
1919
                    }
1920
                }
1921
1922 13
                if ($class->isChangeTrackingNotify()) {
1923
                    // Just treat all properties as changed, there is no other choice.
1924
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1925
                }
1926 13
            }
1927
1928 13
            if ($class->isChangeTrackingDeferredExplicit()) {
1929
                $this->scheduleForDirtyCheck($document);
1930
            }
1931 13
        }
1932
1933 13
        if ($prevManagedCopy !== null) {
1934 6
            $assocField = $assoc['fieldName'];
1935 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1936
1937 6
            if ($assoc['type'] === 'one') {
1938 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1939 2
            } else {
1940 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1941
1942 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1943 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1944 1
                }
1945
            }
1946 6
        }
1947
1948
        // Mark the managed copy visited as well
1949 13
        $visited[spl_object_hash($managedCopy)] = true;
1950
1951 13
        $this->cascadeMerge($document, $managedCopy, $visited);
1952
1953 13
        return $managedCopy;
1954
    }
1955
1956
    /**
1957
     * Detaches a document from the persistence management. It's persistence will
1958
     * no longer be managed by Doctrine.
1959
     *
1960
     * @param object $document The document to detach.
1961
     */
1962 9
    public function detach($document)
1963
    {
1964 9
        $visited = array();
1965 9
        $this->doDetach($document, $visited);
1966 9
    }
1967
1968
    /**
1969
     * Executes a detach operation on the given document.
1970
     *
1971
     * @param object $document
1972
     * @param array $visited
1973
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1974
     */
1975 12
    private function doDetach($document, array &$visited)
1976
    {
1977 12
        $oid = spl_object_hash($document);
1978 12
        if (isset($visited[$oid])) {
1979 4
            return; // Prevent infinite recursion
1980
        }
1981
1982 12
        $visited[$oid] = $document; // mark visited
1983
1984 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1985 12
            case self::STATE_MANAGED:
1986 12
                $this->removeFromIdentityMap($document);
1987 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1988 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1989 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1990 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1991 12
                    $this->hasScheduledCollections[$oid]);
1992 12
                break;
1993 4
            case self::STATE_NEW:
1994 4
            case self::STATE_DETACHED:
1995 4
                return;
1996 12
        }
1997
1998 12
        $this->cascadeDetach($document, $visited);
1999 12
    }
2000
2001
    /**
2002
     * Refreshes the state of the given document from the database, overwriting
2003
     * any local, unpersisted changes.
2004
     *
2005
     * @param object $document The document to refresh.
2006
     * @throws \InvalidArgumentException If the document is not MANAGED.
2007
     */
2008 22
    public function refresh($document)
2009
    {
2010 22
        $visited = array();
2011 22
        $this->doRefresh($document, $visited);
2012 21
    }
2013
2014
    /**
2015
     * Executes a refresh operation on a document.
2016
     *
2017
     * @param object $document The document to refresh.
2018
     * @param array $visited The already visited documents during cascades.
2019
     * @throws \InvalidArgumentException If the document is not MANAGED.
2020
     */
2021 22
    private function doRefresh($document, array &$visited)
2022
    {
2023 22
        $oid = spl_object_hash($document);
2024 22
        if (isset($visited[$oid])) {
2025
            return; // Prevent infinite recursion
2026
        }
2027
2028 22
        $visited[$oid] = $document; // mark visited
2029
2030 22
        $class = $this->dm->getClassMetadata(get_class($document));
2031
2032 22
        if ( ! $class->isEmbeddedDocument) {
2033 22
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2034 21
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2035 21
                $this->getDocumentPersister($class->name)->refresh($id, $document);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Per...entPersister::refresh() has been deprecated with message: The first argument is deprecated.

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

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

Loading history...
2036 21
            } else {
2037 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2038
            }
2039 21
        }
2040
2041 21
        $this->cascadeRefresh($document, $visited);
2042 21
    }
2043
2044
    /**
2045
     * Cascades a refresh operation to associated documents.
2046
     *
2047
     * @param object $document
2048
     * @param array $visited
2049
     */
2050 21
    private function cascadeRefresh($document, array &$visited)
2051
    {
2052 21
        $class = $this->dm->getClassMetadata(get_class($document));
2053
2054 21
        $associationMappings = array_filter(
2055 21
            $class->associationMappings,
2056
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2057 21
        );
2058
2059 21
        foreach ($associationMappings as $mapping) {
2060 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2061 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2062 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2063
                    // Unwrap so that foreach() does not initialize
2064 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2065 15
                }
2066 15
                foreach ($relatedDocuments as $relatedDocument) {
2067
                    $this->doRefresh($relatedDocument, $visited);
2068 15
                }
2069 15
            } elseif ($relatedDocuments !== null) {
2070 2
                $this->doRefresh($relatedDocuments, $visited);
2071 2
            }
2072 21
        }
2073 21
    }
2074
2075
    /**
2076
     * Cascades a detach operation to associated documents.
2077
     *
2078
     * @param object $document
2079
     * @param array $visited
2080
     */
2081 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...
2082
    {
2083 12
        $class = $this->dm->getClassMetadata(get_class($document));
2084 12
        foreach ($class->fieldMappings as $mapping) {
2085 12
            if ( ! $mapping['isCascadeDetach']) {
2086 12
                continue;
2087
            }
2088 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2089 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2090 7
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2091
                    // Unwrap so that foreach() does not initialize
2092 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2093 6
                }
2094 7
                foreach ($relatedDocuments as $relatedDocument) {
2095 5
                    $this->doDetach($relatedDocument, $visited);
2096 7
                }
2097 7
            } elseif ($relatedDocuments !== null) {
2098 5
                $this->doDetach($relatedDocuments, $visited);
2099 5
            }
2100 12
        }
2101 12
    }
2102
    /**
2103
     * Cascades a merge operation to associated documents.
2104
     *
2105
     * @param object $document
2106
     * @param object $managedCopy
2107
     * @param array $visited
2108
     */
2109 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2110
    {
2111 13
        $class = $this->dm->getClassMetadata(get_class($document));
2112
2113 13
        $associationMappings = array_filter(
2114 13
            $class->associationMappings,
2115
            function ($assoc) { return $assoc['isCascadeMerge']; }
2116 13
        );
2117
2118 13
        foreach ($associationMappings as $assoc) {
2119 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2120
2121 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2122 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2123
                    // Collections are the same, so there is nothing to do
2124
                    continue;
2125
                }
2126
2127 8
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2128
                    // Unwrap so that foreach() does not initialize
2129 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2130 6
                }
2131
2132 8
                foreach ($relatedDocuments as $relatedDocument) {
2133 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2134 8
                }
2135 12
            } elseif ($relatedDocuments !== null) {
2136 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2137 3
            }
2138 13
        }
2139 13
    }
2140
2141
    /**
2142
     * Cascades the save operation to associated documents.
2143
     *
2144
     * @param object $document
2145
     * @param array $visited
2146
     */
2147 621
    private function cascadePersist($document, array &$visited)
2148
    {
2149 621
        $class = $this->dm->getClassMetadata(get_class($document));
2150
2151 621
        $associationMappings = array_filter(
2152 621
            $class->associationMappings,
2153
            function ($assoc) { return $assoc['isCascadePersist']; }
2154 621
        );
2155
2156 621
        foreach ($associationMappings as $fieldName => $mapping) {
2157 426
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2158
2159 426
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2160 356
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2161 17
                    if ($relatedDocuments->getOwner() !== $document) {
2162 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2163 2
                    }
2164
                    // Unwrap so that foreach() does not initialize
2165 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2166 17
                }
2167
2168 356
                $count = 0;
2169 356
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2170 196
                    if ( ! empty($mapping['embedded'])) {
2171 119
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2172 119
                        if ($knownParent && $knownParent !== $document) {
2173 4
                            $relatedDocument = clone $relatedDocument;
2174 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2175 4
                        }
2176 119
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2177 119
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2178 119
                    }
2179 196
                    $this->doPersist($relatedDocument, $visited);
2180 355
                }
2181 426
            } elseif ($relatedDocuments !== null) {
2182 126
                if ( ! empty($mapping['embedded'])) {
2183 69
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2184 69
                    if ($knownParent && $knownParent !== $document) {
2185 5
                        $relatedDocuments = clone $relatedDocuments;
2186 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2187 5
                    }
2188 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2189 69
                }
2190 126
                $this->doPersist($relatedDocuments, $visited);
2191 125
            }
2192 620
        }
2193 619
    }
2194
2195
    /**
2196
     * Cascades the delete operation to associated documents.
2197
     *
2198
     * @param object $document
2199
     * @param array $visited
2200
     */
2201 73 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...
2202
    {
2203 73
        $class = $this->dm->getClassMetadata(get_class($document));
2204 73
        foreach ($class->fieldMappings as $mapping) {
2205 72
            if ( ! $mapping['isCascadeRemove']) {
2206 72
                continue;
2207
            }
2208 35
            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...
2209 2
                $document->__load();
2210 2
            }
2211
2212 35
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2213 35
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2214
                // If its a PersistentCollection initialization is intended! No unwrap!
2215 25
                foreach ($relatedDocuments as $relatedDocument) {
2216 14
                    $this->doRemove($relatedDocument, $visited);
2217 25
                }
2218 35
            } elseif ($relatedDocuments !== null) {
2219 12
                $this->doRemove($relatedDocuments, $visited);
2220 12
            }
2221 73
        }
2222 73
    }
2223
2224
    /**
2225
     * Acquire a lock on the given document.
2226
     *
2227
     * @param object $document
2228
     * @param int $lockMode
2229
     * @param int $lockVersion
2230
     * @throws LockException
2231
     * @throws \InvalidArgumentException
2232
     */
2233 9
    public function lock($document, $lockMode, $lockVersion = null)
2234
    {
2235 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2236 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2237
        }
2238
2239 8
        $documentName = get_class($document);
2240 8
        $class = $this->dm->getClassMetadata($documentName);
2241
2242 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2243 3
            if ( ! $class->isVersioned) {
2244 1
                throw LockException::notVersioned($documentName);
2245
            }
2246
2247 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...
2248 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2249 2
                if ($documentVersion != $lockVersion) {
2250 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2251
                }
2252 1
            }
2253 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2254 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2255 5
        }
2256 6
    }
2257
2258
    /**
2259
     * Releases a lock on the given document.
2260
     *
2261
     * @param object $document
2262
     * @throws \InvalidArgumentException
2263
     */
2264 1
    public function unlock($document)
2265
    {
2266 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2267
            throw new \InvalidArgumentException("Document is not MANAGED.");
2268
        }
2269 1
        $documentName = get_class($document);
2270 1
        $this->getDocumentPersister($documentName)->unlock($document);
2271 1
    }
2272
2273
    /**
2274
     * Clears the UnitOfWork.
2275
     *
2276
     * @param string|null $documentName if given, only documents of this type will get detached.
2277
     */
2278 400
    public function clear($documentName = null)
2279
    {
2280 400
        if ($documentName === null) {
2281 394
            $this->identityMap =
2282 394
            $this->documentIdentifiers =
2283 394
            $this->originalDocumentData =
2284 394
            $this->documentChangeSets =
2285 394
            $this->documentStates =
2286 394
            $this->scheduledForDirtyCheck =
2287 394
            $this->documentInsertions =
2288 394
            $this->documentUpserts =
2289 394
            $this->documentUpdates =
2290 394
            $this->documentDeletions =
2291 394
            $this->collectionUpdates =
2292 394
            $this->collectionDeletions =
2293 394
            $this->parentAssociations =
2294 394
            $this->orphanRemovals =
2295 394
            $this->hasScheduledCollections = array();
2296 394
        } else {
2297 6
            $visited = array();
2298 6
            foreach ($this->identityMap as $className => $documents) {
2299 6
                if ($className === $documentName) {
2300 3
                    foreach ($documents as $document) {
2301 3
                        $this->doDetach($document, $visited);
2302 3
                    }
2303 3
                }
2304 6
            }
2305
        }
2306
2307 400 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...
2308
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2309
        }
2310 400
    }
2311
2312
    /**
2313
     * INTERNAL:
2314
     * Schedules an embedded document for removal. The remove() operation will be
2315
     * invoked on that document at the beginning of the next commit of this
2316
     * UnitOfWork.
2317
     *
2318
     * @ignore
2319
     * @param object $document
2320
     */
2321 49
    public function scheduleOrphanRemoval($document)
2322
    {
2323 49
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2324 49
    }
2325
2326
    /**
2327
     * INTERNAL:
2328
     * Unschedules an embedded or referenced object for removal.
2329
     *
2330
     * @ignore
2331
     * @param object $document
2332
     */
2333 110
    public function unscheduleOrphanRemoval($document)
2334
    {
2335 110
        $oid = spl_object_hash($document);
2336 110
        if (isset($this->orphanRemovals[$oid])) {
2337 1
            unset($this->orphanRemovals[$oid]);
2338 1
        }
2339 110
    }
2340
2341
    /**
2342
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2343
     *  1) sets owner if it was cloned
2344
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2345
     *  3) NOP if state is OK
2346
     * Returned collection should be used from now on (only important with 2nd point)
2347
     *
2348
     * @param PersistentCollectionInterface $coll
2349
     * @param object $document
2350
     * @param ClassMetadata $class
2351
     * @param string $propName
2352
     * @return PersistentCollectionInterface
2353
     */
2354 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2355
    {
2356 8
        $owner = $coll->getOwner();
2357 8
        if ($owner === null) { // cloned
2358 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2359 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2360 2
            if ( ! $coll->isInitialized()) {
2361 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2362 1
            }
2363 2
            $newValue = clone $coll;
2364 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2365 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2366 2
            if ($this->isScheduledForUpdate($document)) {
2367
                // @todo following line should be superfluous once collections are stored in change sets
2368
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2369
            }
2370 2
            return $newValue;
2371
        }
2372 6
        return $coll;
2373
    }
2374
2375
    /**
2376
     * INTERNAL:
2377
     * Schedules a complete collection for removal when this UnitOfWork commits.
2378
     *
2379
     * @param PersistentCollectionInterface $coll
2380
     */
2381 42
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2382
    {
2383 42
        $oid = spl_object_hash($coll);
2384 42
        unset($this->collectionUpdates[$oid]);
2385 42
        if ( ! isset($this->collectionDeletions[$oid])) {
2386 42
            $this->collectionDeletions[$oid] = $coll;
2387 42
            $this->scheduleCollectionOwner($coll);
2388 42
        }
2389 42
    }
2390
2391
    /**
2392
     * Checks whether a PersistentCollection is scheduled for deletion.
2393
     *
2394
     * @param PersistentCollectionInterface $coll
2395
     * @return boolean
2396
     */
2397 216
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2398
    {
2399 216
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2400
    }
2401
2402
    /**
2403
     * INTERNAL:
2404
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2405
     *
2406
     * @param PersistentCollectionInterface $coll
2407
     */
2408 218 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

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

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

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

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

Loading history...
2603 15
            $discriminatorValue = $data[$class->discriminatorField];
2604 198
        } elseif ($class->defaultDiscriminatorValue !== null) {
2605 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2606 1
        }
2607
2608 198
        if ($discriminatorValue !== null) {
2609 16
            return isset($class->discriminatorMap[$discriminatorValue])
2610 16
                ? $class->discriminatorMap[$discriminatorValue]
2611 16
                : $discriminatorValue;
2612
        }
2613
2614 182
        return $mapping['targetDocument'];
2615
    }
2616
2617
    /**
2618
     * INTERNAL:
2619
     * Creates a document. Used for reconstitution of documents during hydration.
2620
     *
2621
     * @ignore
2622
     * @param string $className The name of the document class.
2623
     * @param array $data The data for the document.
2624
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2625
     * @param object $document The document to be hydrated into in case of creation
2626
     * @return object The document instance.
2627
     * @internal Highly performance-sensitive method.
2628
     */
2629 406
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2630
    {
2631 406
        $class = $this->dm->getClassMetadata($className);
2632
2633
        // @TODO figure out how to remove this
2634 406
        $discriminatorValue = null;
2635 406 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...
2636 19
            $discriminatorValue = $data[$class->discriminatorField];
2637 406
        } elseif (isset($class->defaultDiscriminatorValue)) {
2638 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2639 2
        }
2640
2641 406
        if ($discriminatorValue !== null) {
2642 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2643 20
                ? $class->discriminatorMap[$discriminatorValue]
2644 20
                : $discriminatorValue;
2645
2646 20
            $class = $this->dm->getClassMetadata($className);
2647
2648 20
            unset($data[$class->discriminatorField]);
2649 20
        }
2650
        
2651 406
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2652 1
            $document = $class->newInstance();
2653 1
            $this->hydratorFactory->hydrate($document, $data, $hints);
2654 1
            return $document;
2655
        }
2656
2657 405
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2658 405
        $serializedId = serialize($id);
2659
2660 405
        if (isset($this->identityMap[$class->name][$serializedId])) {
2661 103
            $document = $this->identityMap[$class->name][$serializedId];
2662 103
            $oid = spl_object_hash($document);
2663 103
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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