Completed
Pull Request — master (#1105)
by
unknown
16:26 queued 10s
created

UnitOfWork::initializeObject()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

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

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

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

Loading history...
382
            $options = array_merge($defaultOptions, $options);
383
        } else {
384 571
            $options = $defaultOptions;
385
        }
386
        // Compute changes done since last commit.
387 571
        if ($document === null) {
388 565
            $this->computeChangeSets();
389 570
        } elseif (is_object($document)) {
390 12
            $this->computeSingleDocumentChangeSet($document);
391 12
        } elseif (is_array($document)) {
392 1
            foreach ($document as $object) {
393 1
                $this->computeSingleDocumentChangeSet($object);
394 1
            }
395 1
        }
396
397 569
        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...
398 243
            $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...
399 204
            $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...
400 194
            $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...
401 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...
402 25
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1889
                                }
1890
                            }
1891
1892
                            $prop->setValue($managedCopy, $other);
1893
                        }
1894 4
                    } else {
1895 10
                        $mergeCol = $prop->getValue($document);
1896
1897 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
1898
                            /* Do not merge fields marked lazy that have not
1899
                             * been fetched. Keep the lazy persistent collection
1900
                             * of the managed copy.
1901
                             */
1902 3
                            continue;
1903
                        }
1904
1905 7
                        $managedCol = $prop->getValue($managedCopy);
1906
1907 7
                        if ( ! $managedCol) {
1908 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
1909 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1910 2
                            $prop->setValue($managedCopy, $managedCol);
1911 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1912 2
                        }
1913
1914
                        /* Note: do not process association's target documents.
1915
                         * They will be handled during the cascade. Initialize
1916
                         * and, if necessary, clear $managedCol for now.
1917
                         */
1918 7
                        if ($assoc2['isCascadeMerge']) {
1919 7
                            $managedCol->initialize();
1920
1921
                            // If $managedCol differs from the merged collection, clear and set dirty
1922 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1923 2
                                $managedCol->unwrap()->clear();
1924 2
                                $managedCol->setDirty(true);
1925
1926 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1927
                                    $this->scheduleForDirtyCheck($managedCopy);
1928
                                }
1929 2
                            }
1930 7
                        }
1931
                    }
1932
                }
1933
1934 13
                if ($class->isChangeTrackingNotify()) {
1935
                    // Just treat all properties as changed, there is no other choice.
1936
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1937
                }
1938 13
            }
1939
1940 13
            if ($class->isChangeTrackingDeferredExplicit()) {
1941
                $this->scheduleForDirtyCheck($document);
1942
            }
1943 13
        }
1944
1945 13
        if ($prevManagedCopy !== null) {
1946 6
            $assocField = $assoc['fieldName'];
1947 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1948
1949 6
            if ($assoc['type'] === 'one') {
1950 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1951 2
            } else {
1952 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1953
1954 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1955 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1956 1
                }
1957
            }
1958 6
        }
1959
1960
        // Mark the managed copy visited as well
1961 13
        $visited[spl_object_hash($managedCopy)] = true;
1962
1963 13
        $this->cascadeMerge($document, $managedCopy, $visited);
1964
1965 13
        return $managedCopy;
1966
    }
1967
1968
    /**
1969
     * Detaches a document from the persistence management. It's persistence will
1970
     * no longer be managed by Doctrine.
1971
     *
1972
     * @param object $document The document to detach.
1973
     */
1974 9
    public function detach($document)
1975
    {
1976 9
        $visited = array();
1977 9
        $this->doDetach($document, $visited);
1978 9
    }
1979
1980
    /**
1981
     * Executes a detach operation on the given document.
1982
     *
1983
     * @param object $document
1984
     * @param array $visited
1985
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1986
     */
1987 12
    private function doDetach($document, array &$visited)
1988
    {
1989 12
        $oid = spl_object_hash($document);
1990 12
        if (isset($visited[$oid])) {
1991 4
            return; // Prevent infinite recursion
1992
        }
1993
1994 12
        $visited[$oid] = $document; // mark visited
1995
1996 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1997 12
            case self::STATE_MANAGED:
1998 12
                $this->removeFromIdentityMap($document);
1999 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2000 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2001 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2002 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2003 12
                    $this->hasScheduledCollections[$oid]);
2004 12
                break;
2005 4
            case self::STATE_NEW:
2006 4
            case self::STATE_DETACHED:
2007 4
                return;
2008 12
        }
2009
2010 12
        $this->cascadeDetach($document, $visited);
2011 12
    }
2012
2013
    /**
2014
     * Refreshes the state of the given document from the database, overwriting
2015
     * any local, unpersisted changes.
2016
     *
2017
     * @param object $document The document to refresh.
2018
     * @throws \InvalidArgumentException If the document is not MANAGED.
2019
     */
2020 21
    public function refresh($document)
2021
    {
2022 21
        $visited = array();
2023 21
        $this->doRefresh($document, $visited);
2024 20
    }
2025
2026
    /**
2027
     * Executes a refresh operation on a document.
2028
     *
2029
     * @param object $document The document to refresh.
2030
     * @param array $visited The already visited documents during cascades.
2031
     * @throws \InvalidArgumentException If the document is not MANAGED.
2032
     */
2033 21
    private function doRefresh($document, array &$visited)
2034
    {
2035 21
        $oid = spl_object_hash($document);
2036 21
        if (isset($visited[$oid])) {
2037
            return; // Prevent infinite recursion
2038
        }
2039
2040 21
        $visited[$oid] = $document; // mark visited
2041
2042 21
        $class = $this->dm->getClassMetadata(get_class($document));
2043
2044 21
        if ( ! $class->isEmbeddedDocument) {
2045 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2046 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2047 20
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2048 20
            } else {
2049 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2050
            }
2051 20
        }
2052
2053 20
        $this->cascadeRefresh($document, $visited);
2054 20
    }
2055
2056
    /**
2057
     * Cascades a refresh operation to associated documents.
2058
     *
2059
     * @param object $document
2060
     * @param array $visited
2061
     */
2062 20
    private function cascadeRefresh($document, array &$visited)
2063
    {
2064 20
        $class = $this->dm->getClassMetadata(get_class($document));
2065
2066 20
        $associationMappings = array_filter(
2067 20
            $class->associationMappings,
2068
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2069 20
        );
2070
2071 20
        foreach ($associationMappings as $mapping) {
2072 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2073 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2074 15
                if ($relatedDocuments instanceof PersistentCollection) {
2075
                    // Unwrap so that foreach() does not initialize
2076 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2077 15
                }
2078 15
                foreach ($relatedDocuments as $relatedDocument) {
2079
                    $this->doRefresh($relatedDocument, $visited);
2080 15
                }
2081 15
            } elseif ($relatedDocuments !== null) {
2082 2
                $this->doRefresh($relatedDocuments, $visited);
2083 2
            }
2084 20
        }
2085 20
    }
2086
2087
    /**
2088
     * Cascades a detach operation to associated documents.
2089
     *
2090
     * @param object $document
2091
     * @param array $visited
2092
     */
2093 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...
2094
    {
2095 12
        $class = $this->dm->getClassMetadata(get_class($document));
2096 12
        foreach ($class->fieldMappings as $mapping) {
2097 12
            if ( ! $mapping['isCascadeDetach']) {
2098 12
                continue;
2099
            }
2100 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2101 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2102 7
                if ($relatedDocuments instanceof PersistentCollection) {
2103
                    // Unwrap so that foreach() does not initialize
2104 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2105 6
                }
2106 7
                foreach ($relatedDocuments as $relatedDocument) {
2107 5
                    $this->doDetach($relatedDocument, $visited);
2108 7
                }
2109 7
            } elseif ($relatedDocuments !== null) {
2110 5
                $this->doDetach($relatedDocuments, $visited);
2111 5
            }
2112 12
        }
2113 12
    }
2114
    /**
2115
     * Cascades a merge operation to associated documents.
2116
     *
2117
     * @param object $document
2118
     * @param object $managedCopy
2119
     * @param array $visited
2120
     */
2121 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2122
    {
2123 13
        $class = $this->dm->getClassMetadata(get_class($document));
2124
2125 13
        $associationMappings = array_filter(
2126 13
            $class->associationMappings,
2127
            function ($assoc) { return $assoc['isCascadeMerge']; }
2128 13
        );
2129
2130 13
        foreach ($associationMappings as $assoc) {
2131 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2132
2133 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2134 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2135
                    // Collections are the same, so there is nothing to do
2136
                    continue;
2137
                }
2138
2139 8
                if ($relatedDocuments instanceof PersistentCollection) {
2140
                    // Unwrap so that foreach() does not initialize
2141 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2142 6
                }
2143
2144 8
                foreach ($relatedDocuments as $relatedDocument) {
2145 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2146 8
                }
2147 12
            } elseif ($relatedDocuments !== null) {
2148 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2149 3
            }
2150 13
        }
2151 13
    }
2152
2153
    /**
2154
     * Cascades the save operation to associated documents.
2155
     *
2156
     * @param object $document
2157
     * @param array $visited
2158
     */
2159 589
    private function cascadePersist($document, array &$visited)
2160
    {
2161 589
        $class = $this->dm->getClassMetadata(get_class($document));
2162
2163 589
        $associationMappings = array_filter(
2164 589
            $class->associationMappings,
2165
            function ($assoc) { return $assoc['isCascadePersist']; }
2166 589
        );
2167
2168 589
        foreach ($associationMappings as $fieldName => $mapping) {
2169 405
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2170
2171 405
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2172 348
                if ($relatedDocuments instanceof PersistentCollection) {
2173 17
                    if ($relatedDocuments->getOwner() !== $document) {
2174 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2175 2
                    }
2176
                    // Unwrap so that foreach() does not initialize
2177 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2178 17
                }
2179
2180 348
                $count = 0;
2181 348
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2182 191
                    if ( ! empty($mapping['embedded'])) {
2183 115
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2184 115
                        if ($knownParent && $knownParent !== $document) {
2185 4
                            $relatedDocument = clone $relatedDocument;
2186 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2187 4
                        }
2188 115
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2189 115
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2190 115
                    }
2191 191
                    $this->doPersist($relatedDocument, $visited);
2192 347
                }
2193 405
            } elseif ($relatedDocuments !== null) {
2194 124
                if ( ! empty($mapping['embedded'])) {
2195 67
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2196 67
                    if ($knownParent && $knownParent !== $document) {
2197 5
                        $relatedDocuments = clone $relatedDocuments;
2198 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2199 5
                    }
2200 67
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2201 67
                }
2202 124
                $this->doPersist($relatedDocuments, $visited);
2203 123
            }
2204 588
        }
2205 587
    }
2206
2207
    /**
2208
     * Cascades the delete operation to associated documents.
2209
     *
2210
     * @param object $document
2211
     * @param array $visited
2212
     */
2213 68 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...
2214
    {
2215 68
        $class = $this->dm->getClassMetadata(get_class($document));
2216 68
        foreach ($class->fieldMappings as $mapping) {
2217 67
            if ( ! $mapping['isCascadeRemove']) {
2218 67
                continue;
2219
            }
2220 34
            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...
2221 2
                $document->__load();
2222 2
            }
2223
2224 34
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2225 34
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2226
                // If its a PersistentCollection initialization is intended! No unwrap!
2227 25
                foreach ($relatedDocuments as $relatedDocument) {
2228 14
                    $this->doRemove($relatedDocument, $visited);
2229 25
                }
2230 34
            } elseif ($relatedDocuments !== null) {
2231 12
                $this->doRemove($relatedDocuments, $visited);
2232 12
            }
2233 68
        }
2234 68
    }
2235
2236
    /**
2237
     * Acquire a lock on the given document.
2238
     *
2239
     * @param object $document
2240
     * @param int $lockMode
2241
     * @param int $lockVersion
2242
     * @throws LockException
2243
     * @throws \InvalidArgumentException
2244
     */
2245 9
    public function lock($document, $lockMode, $lockVersion = null)
2246
    {
2247 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2248 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2249
        }
2250
2251 8
        $documentName = get_class($document);
2252 8
        $class = $this->dm->getClassMetadata($documentName);
2253
2254 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2255 3
            if ( ! $class->isVersioned) {
2256 1
                throw LockException::notVersioned($documentName);
2257
            }
2258
2259 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...
2260 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2261 2
                if ($documentVersion != $lockVersion) {
2262 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2263
                }
2264 1
            }
2265 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2266 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2267 5
        }
2268 6
    }
2269
2270
    /**
2271
     * Releases a lock on the given document.
2272
     *
2273
     * @param object $document
2274
     * @throws \InvalidArgumentException
2275
     */
2276 1
    public function unlock($document)
2277
    {
2278 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2279
            throw new \InvalidArgumentException("Document is not MANAGED.");
2280
        }
2281 1
        $documentName = get_class($document);
2282 1
        $this->getDocumentPersister($documentName)->unlock($document);
2283 1
    }
2284
2285
    /**
2286
     * Clears the UnitOfWork.
2287
     *
2288
     * @param string|null $documentName if given, only documents of this type will get detached.
2289
     */
2290 394
    public function clear($documentName = null)
2291
    {
2292 394
        if ($documentName === null) {
2293 388
            $this->identityMap =
2294 388
            $this->documentIdentifiers =
2295 388
            $this->originalDocumentData =
2296 388
            $this->documentChangeSets =
2297 388
            $this->documentStates =
2298 388
            $this->scheduledForDirtyCheck =
2299 388
            $this->documentInsertions =
2300 388
            $this->documentUpserts =
2301 388
            $this->documentUpdates =
2302 388
            $this->documentDeletions =
2303 388
            $this->collectionUpdates =
2304 388
            $this->collectionDeletions =
2305 388
            $this->parentAssociations =
2306 388
            $this->orphanRemovals =
2307 388
            $this->hasScheduledCollections = array();
2308 388
        } else {
2309 6
            $visited = array();
2310 6
            foreach ($this->identityMap as $className => $documents) {
2311 6
                if ($className === $documentName) {
2312 3
                    foreach ($documents as $document) {
2313 3
                        $this->doDetach($document, $visited);
2314 3
                    }
2315 3
                }
2316 6
            }
2317
        }
2318
2319 394 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...
2320
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2321
        }
2322 394
    }
2323
2324
    /**
2325
     * INTERNAL:
2326
     * Schedules an embedded document for removal. The remove() operation will be
2327
     * invoked on that document at the beginning of the next commit of this
2328
     * UnitOfWork.
2329
     *
2330
     * @ignore
2331
     * @param object $document
2332
     */
2333 49
    public function scheduleOrphanRemoval($document)
2334
    {
2335 49
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2336 49
    }
2337
2338
    /**
2339
     * INTERNAL:
2340
     * Unschedules an embedded or referenced object for removal.
2341
     *
2342
     * @ignore
2343
     * @param object $document
2344
     */
2345 104
    public function unscheduleOrphanRemoval($document)
2346
    {
2347 104
        $oid = spl_object_hash($document);
2348 104
        if (isset($this->orphanRemovals[$oid])) {
2349 1
            unset($this->orphanRemovals[$oid]);
2350 1
        }
2351 104
    }
2352
2353
    /**
2354
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2355
     *  1) sets owner if it was cloned
2356
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2357
     *  3) NOP if state is OK
2358
     * Returned collection should be used from now on (only important with 2nd point)
2359
     *
2360
     * @param PersistentCollection $coll
2361
     * @param object $document
2362
     * @param ClassMetadata $class
2363
     * @param string $propName
2364
     * @return PersistentCollection
2365
     */
2366 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2367
    {
2368 8
        $owner = $coll->getOwner();
2369 8
        if ($owner === null) { // cloned
2370 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2371 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2372 2
            if ( ! $coll->isInitialized()) {
2373 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2374 1
            }
2375 2
            $newValue = clone $coll;
2376 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2377 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2378 2
            if ($this->isScheduledForUpdate($document)) {
2379
                // @todo following line should be superfluous once collections are stored in change sets
2380
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2381
            }
2382 2
            return $newValue;
2383
        }
2384 6
        return $coll;
2385
    }
2386
2387
    /**
2388
     * INTERNAL:
2389
     * Schedules a complete collection for removal when this UnitOfWork commits.
2390
     *
2391
     * @param PersistentCollection $coll
2392
     */
2393 42
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2394
    {
2395 42
        $oid = spl_object_hash($coll);
2396 42
        unset($this->collectionUpdates[$oid]);
2397 42
        if ( ! isset($this->collectionDeletions[$oid])) {
2398 42
            $this->collectionDeletions[$oid] = $coll;
2399 42
            $this->scheduleCollectionOwner($coll);
2400 42
        }
2401 42
    }
2402
2403
    /**
2404
     * Checks whether a PersistentCollection is scheduled for deletion.
2405
     *
2406
     * @param PersistentCollection $coll
2407
     * @return boolean
2408
     */
2409 208
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2410
    {
2411 208
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2412
    }
2413
2414
    /**
2415
     * INTERNAL:
2416
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2417
     *
2418
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2419
     */
2420 211 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
2459
    {
2460 211
        $oid = spl_object_hash($coll);
2461 211
        if (isset($this->collectionUpdates[$oid])) {
2462 201
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2463 201
            unset($this->collectionUpdates[$oid]);
2464 201
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2465 201
        }
2466 211
    }
2467
2468
    /**
2469
     * Checks whether a PersistentCollection is scheduled for update.
2470
     *
2471
     * @param PersistentCollection $coll
2472
     * @return boolean
2473
     */
2474 124
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2475
    {
2476 124
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2477
    }
2478
2479
    /**
2480
     * INTERNAL:
2481
     * Gets PersistentCollections that have been visited during computing change
2482
     * set of $document
2483
     *
2484
     * @param object $document
2485
     * @return PersistentCollection[]
2486
     */
2487 551
    public function getVisitedCollections($document)
2488
    {
2489 551
        $oid = spl_object_hash($document);
2490 551
        return isset($this->visitedCollections[$oid])
2491 551
                ? $this->visitedCollections[$oid]
2492 551
                : array();
2493
    }
2494
2495
    /**
2496
     * INTERNAL:
2497
     * Gets PersistentCollections that are scheduled to update and related to $document
2498
     *
2499
     * @param object $document
2500
     * @return array
2501
     */
2502 551
    public function getScheduledCollections($document)
2503
    {
2504 551
        $oid = spl_object_hash($document);
2505 551
        return isset($this->hasScheduledCollections[$oid])
2506 551
                ? $this->hasScheduledCollections[$oid]
2507 551
                : array();
2508
    }
2509
2510
    /**
2511
     * Checks whether the document is related to a PersistentCollection
2512
     * scheduled for update or deletion.
2513
     *
2514
     * @param object $document
2515
     * @return boolean
2516
     */
2517 49
    public function hasScheduledCollections($document)
2518
    {
2519 49
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2520
    }
2521
2522
    /**
2523
     * Marks the PersistentCollection's top-level owner as having a relation to
2524
     * a collection scheduled for update or deletion.
2525
     *
2526
     * If the owner is not scheduled for any lifecycle action, it will be
2527
     * scheduled for update to ensure that versioning takes place if necessary.
2528
     *
2529
     * If the collection is nested within atomic collection, it is immediately
2530
     * unscheduled and atomic one is scheduled for update instead. This makes
2531
     * calculating update data way easier.
2532
     *
2533
     * @param PersistentCollection $coll
2534
     */
2535 229
    private function scheduleCollectionOwner(PersistentCollection $coll)
2536
    {
2537 229
        $document = $this->getOwningDocument($coll->getOwner());
2538 229
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2539
2540 229
        if ($document !== $coll->getOwner()) {
2541 25
            $parent = $coll->getOwner();
2542 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2543 25
                list($mapping, $parent, ) = $parentAssoc;
2544 25
            }
2545 25
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2546 8
                $class = $this->dm->getClassMetadata(get_class($document));
2547 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...
2548 8
                $this->scheduleCollectionUpdate($atomicCollection);
2549 8
                $this->unscheduleCollectionDeletion($coll);
2550 8
                $this->unscheduleCollectionUpdate($coll);
2551 8
            }
2552 25
        }
2553
2554 229
        if ( ! $this->isDocumentScheduled($document)) {
2555 48
            $this->scheduleForUpdate($document);
2556 48
        }
2557 229
    }
2558
2559
    /**
2560
     * Get the top-most owning document of a given document
2561
     *
2562
     * If a top-level document is provided, that same document will be returned.
2563
     * For an embedded document, we will walk through parent associations until
2564
     * we find a top-level document.
2565
     *
2566
     * @param object $document
2567
     * @throws \UnexpectedValueException when a top-level document could not be found
2568
     * @return object
2569
     */
2570 231
    public function getOwningDocument($document)
2571
    {
2572 231
        $class = $this->dm->getClassMetadata(get_class($document));
2573 231
        while ($class->isEmbeddedDocument) {
2574 39
            $parentAssociation = $this->getParentAssociation($document);
2575
2576 39
            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...
2577
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2578
            }
2579
2580 39
            list(, $document, ) = $parentAssociation;
2581 39
            $class = $this->dm->getClassMetadata(get_class($document));
2582 39
        }
2583
2584 231
        return $document;
2585
    }
2586
2587
    /**
2588
     * Gets the class name for an association (embed or reference) with respect
2589
     * to any discriminator value.
2590
     *
2591
     * @param array      $mapping Field mapping for the association
2592
     * @param array|null $data    Data for the embedded document or reference
2593
     */
2594 209
    public function getClassNameForAssociation(array $mapping, $data)
2595
    {
2596 209
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2597
2598 209
        $discriminatorValue = null;
2599 209
        if (isset($discriminatorField, $data[$discriminatorField])) {
2600 21
            $discriminatorValue = $data[$discriminatorField];
2601 209
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2602
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2603
        }
2604
2605 209
        if ($discriminatorValue !== null) {
2606 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2607 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2608 21
                : $discriminatorValue;
2609
        }
2610
2611 189
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2612
2613 189 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...
2614 15
            $discriminatorValue = $data[$class->discriminatorField];
2615 189
        } elseif ($class->defaultDiscriminatorValue !== null) {
2616 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2617 1
        }
2618
2619 189
        if ($discriminatorValue !== null) {
2620 16
            return isset($class->discriminatorMap[$discriminatorValue])
2621 16
                ? $class->discriminatorMap[$discriminatorValue]
2622 16
                : $discriminatorValue;
2623
        }
2624
2625 173
        return $mapping['targetDocument'];
2626
    }
2627
2628
    /**
2629
     * INTERNAL:
2630
     * Creates a document. Used for reconstitution of documents during hydration.
2631
     *
2632
     * @ignore
2633
     * @param string $className The name of the document class.
2634
     * @param array $data The data for the document.
2635
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2636
     * @param object The document to be hydrated into in case of creation
2637
     * @return object The document instance.
2638
     * @internal Highly performance-sensitive method.
2639
     */
2640 395
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2641
    {
2642 395
        $class = $this->dm->getClassMetadata($className);
2643
2644
        // @TODO figure out how to remove this
2645 395
        $discriminatorValue = null;
2646 395 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...
2647 21
            $discriminatorValue = $data[$class->discriminatorField];
2648 395
        } elseif (isset($class->defaultDiscriminatorValue)) {
2649 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2650 2
        }
2651
2652 395
        if ($discriminatorValue !== null) {
2653 22
            $className = isset($class->discriminatorMap[$discriminatorValue])
2654 22
                ? $class->discriminatorMap[$discriminatorValue]
2655 22
                : $discriminatorValue;
2656
2657 22
            $class = $this->dm->getClassMetadata($className);
2658
2659 22
            unset($data[$class->discriminatorField]);
2660 22
        }
2661
2662 395
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2663 395
        $serializedId = serialize($id);
2664
2665 395
        if (isset($this->identityMap[$class->name][$serializedId])) {
2666 97
            $document = $this->identityMap[$class->name][$serializedId];
2667 97
            $oid = spl_object_hash($document);
2668 97
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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