Completed
Pull Request — master (#1263)
by Andreas
14:36
created

UnitOfWork::getOrCreateDocument()   D

Complexity

Conditions 15
Paths 261

Size

Total Lines 72
Code Lines 47

Duplication

Lines 5
Ratio 6.94 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 3
Bugs 0 Features 2
Metric Value
c 3
b 0
f 2
dl 5
loc 72
ccs 0
cts 55
cp 0
rs 4.1144
cc 15
eloc 47
nc 261
nop 4
crap 240

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\EventManager;
25
use Doctrine\Common\NotifyPropertyChanged;
26
use Doctrine\Common\PropertyChangedListener;
27
use Doctrine\MongoDB\GridFSFile;
28
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
29
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
30
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
31
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
32
use Doctrine\ODM\MongoDB\Proxy\Proxy;
33
use Doctrine\ODM\MongoDB\Query\Query;
34
use Doctrine\ODM\MongoDB\Types\Type;
35
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
36
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
37
38
/**
39
 * The UnitOfWork is responsible for tracking changes to objects during an
40
 * "object-level" transaction and for writing out changes to the database
41
 * in the correct order.
42
 *
43
 * @since       1.0
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = array();
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = array();
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @var array
101
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
102
     *           A value will only really be copied if the value in the document is modified
103
     *           by the user.
104
     */
105
    private $originalDocumentData = array();
106
107
    /**
108
     * Map of document changes. Keys are object ids (spl_object_hash).
109
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
110
     *
111
     * @var array
112
     */
113
    private $documentChangeSets = array();
114
115
    /**
116
     * The (cached) states of any known documents.
117
     * Keys are object ids (spl_object_hash).
118
     *
119
     * @var array
120
     */
121
    private $documentStates = array();
122
123
    /**
124
     * Map of documents that are scheduled for dirty checking at commit time.
125
     *
126
     * Documents are grouped by their class name, and then indexed by their SPL
127
     * object hash. This is only used for documents with a change tracking
128
     * policy of DEFERRED_EXPLICIT.
129
     *
130
     * @var array
131
     * @todo rename: scheduledForSynchronization
132
     */
133
    private $scheduledForDirtyCheck = array();
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = array();
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = array();
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = array();
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = array();
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = array();
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = array();
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = array();
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = array();
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = array();
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = array();
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents
244
     *
245
     * @todo We might need to clean up this array in clear(), doDetach(), etc.
246
     * @var array
247
     */
248
    private $parentAssociations = array();
249
250
    /**
251
     * @var LifecycleEventManager
252
     */
253
    private $lifecycleEventManager;
254
255
    /**
256
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
257
     *
258
     * @param DocumentManager $dm
259
     * @param EventManager $evm
260
     * @param HydratorFactory $hydratorFactory
261
     */
262 919
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
263
    {
264 919
        $this->dm = $dm;
265 919
        $this->evm = $evm;
266 919
        $this->hydratorFactory = $hydratorFactory;
267 919
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
268 919
    }
269
270
    /**
271
     * Factory for returning new PersistenceBuilder instances used for preparing data into
272
     * queries for insert persistence.
273
     *
274
     * @return PersistenceBuilder $pb
275
     */
276 589
    public function getPersistenceBuilder()
277
    {
278 589
        if ( ! $this->persistenceBuilder) {
279 589
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
280 589
        }
281 589
        return $this->persistenceBuilder;
282
    }
283
284
    /**
285
     * Sets the parent association for a given embedded document.
286
     *
287
     * @param object $document
288
     * @param array $mapping
289
     * @param object $parent
290
     * @param string $propertyPath
291
     */
292 160
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
293
    {
294 160
        $oid = spl_object_hash($document);
295 160
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
296 160
    }
297
298
    /**
299
     * Gets the parent association for a given embedded document.
300
     *
301
     *     <code>
302
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
303
     *     </code>
304
     *
305
     * @param object $document
306
     * @return array $association
307
     */
308 157
    public function getParentAssociation($document)
309
    {
310 157
        $oid = spl_object_hash($document);
311 157
        if ( ! isset($this->parentAssociations[$oid])) {
312 155
            return null;
313
        }
314 152
        return $this->parentAssociations[$oid];
315
    }
316
317
    /**
318
     * Get the document persister instance for the given document name
319
     *
320
     * @param string $documentName
321
     * @return Persisters\DocumentPersister
322
     */
323 587
    public function getDocumentPersister($documentName)
324
    {
325 587
        if ( ! isset($this->persisters[$documentName])) {
326 587
            $class = $this->dm->getClassMetadata($documentName);
327 587
            $pb = $this->getPersistenceBuilder();
328 587
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
329
        }
330
        return $this->persisters[$documentName];
331
    }
332
333
    /**
334
     * Get the collection persister instance.
335
     *
336
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
337
     */
338
    public function getCollectionPersister()
339
    {
340
        if ( ! isset($this->collectionPersister)) {
341
            $pb = $this->getPersistenceBuilder();
342
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
343
        }
344
        return $this->collectionPersister;
345
    }
346
347
    /**
348
     * Set the document persister instance to use for the given document name
349
     *
350
     * @param string $documentName
351
     * @param Persisters\DocumentPersister $persister
352
     */
353
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
354
    {
355
        $this->persisters[$documentName] = $persister;
356
    }
357
358
    /**
359
     * Commits the UnitOfWork, executing all operations that have been postponed
360
     * up to this point. The state of all managed documents will be synchronized with
361
     * the database.
362
     *
363
     * The operations are executed in the following order:
364
     *
365
     * 1) All document insertions
366
     * 2) All document updates
367
     * 3) All document deletions
368
     *
369
     * @param object $document
370
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
371
     */
372 542
    public function commit($document = null, array $options = array())
373
    {
374
        // Raise preFlush
375 542
        if ($this->evm->hasListeners(Events::preFlush)) {
376
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
377
        }
378
379 542
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
380 542
        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...
381
            $options = array_merge($defaultOptions, $options);
382
        } else {
383 542
            $options = $defaultOptions;
384
        }
385
        // Compute changes done since last commit.
386 542
        if ($document === null) {
387 537
            $this->computeChangeSets();
388 541
        } elseif (is_object($document)) {
389 4
            $this->computeSingleDocumentChangeSet($document);
390 4
        } elseif (is_array($document)) {
391 1
            foreach ($document as $object) {
392 1
                $this->computeSingleDocumentChangeSet($object);
393 1
            }
394 1
        }
395
396 540
        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...
397 61
            $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...
398 1
            $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...
399 1
            $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...
400 1
            $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...
401 1
            $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...
402 1
            $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...
403 540
        ) {
404 1
            return; // Nothing to do.
405
        }
406
407 539
        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...
408
            foreach ($this->orphanRemovals as $removal) {
409
                $this->remove($removal);
410
            }
411
        }
412
413
        // Raise onFlush
414 539
        if ($this->evm->hasListeners(Events::onFlush)) {
415 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
416 7
        }
417
418 539
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
419 73
            list($class, $documents) = $classAndDocuments;
420 73
            $this->executeUpserts($class, $documents, $options);
421 466
        }
422
423 466
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
424 465
            list($class, $documents) = $classAndDocuments;
425 465
            $this->executeInserts($class, $documents, $options);
426 1
        }
427
428 1
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
429
            list($class, $documents) = $classAndDocuments;
430
            $this->executeUpdates($class, $documents, $options);
431 1
        }
432
433 1
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
434
            list($class, $documents) = $classAndDocuments;
435
            $this->executeDeletions($class, $documents, $options);
436 1
        }
437
438
        // Raise postFlush
439 1
        if ($this->evm->hasListeners(Events::postFlush)) {
440
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
441
        }
442
443
        // Clear up
444 1
        $this->documentInsertions =
445 1
        $this->documentUpserts =
446 1
        $this->documentUpdates =
447 1
        $this->documentDeletions =
448 1
        $this->documentChangeSets =
449 1
        $this->collectionUpdates =
450 1
        $this->collectionDeletions =
451 1
        $this->visitedCollections =
452 1
        $this->scheduledForDirtyCheck =
453 1
        $this->orphanRemovals =
454 1
        $this->hasScheduledCollections = array();
455 1
    }
456
457
    /**
458
     * Groups a list of scheduled documents by their class.
459
     *
460
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
461
     * @param bool $includeEmbedded
462
     * @return array Tuples of ClassMetadata and a corresponding array of objects
463
     */
464 539
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
465
    {
466 539
        if (empty($documents)) {
467 466
            return array();
468
        }
469 538
        $divided = array();
470 538
        $embeds = array();
471 538
        foreach ($documents as $oid => $d) {
472 538
            $className = get_class($d);
473 538
            if (isset($embeds[$className])) {
474 55
                continue;
475
            }
476 538
            if (isset($divided[$className])) {
477 111
                $divided[$className][1][$oid] = $d;
478 111
                continue;
479
            }
480 538
            $class = $this->dm->getClassMetadata($className);
481 538
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
482 147
                $embeds[$className] = true;
483 147
                continue;
484
            }
485 538
            if (empty($divided[$class->name])) {
486 538
                $divided[$class->name] = array($class, array($oid => $d));
487 538
            } else {
488
                $divided[$class->name][1][$oid] = $d;
489
            }
490 538
        }
491 538
        return $divided;
492
    }
493
494
    /**
495
     * Compute changesets of all documents scheduled for insertion.
496
     *
497
     * Embedded documents will not be processed.
498
     */
499 544 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...
500
    {
501 544
        foreach ($this->documentInsertions as $document) {
502 483
            $class = $this->dm->getClassMetadata(get_class($document));
503 483
            if ( ! $class->isEmbeddedDocument) {
504 480
                $this->computeChangeSet($class, $document);
505 479
            }
506 543
        }
507 543
    }
508
509
    /**
510
     * Compute changesets of all documents scheduled for upsert.
511
     *
512
     * Embedded documents will not be processed.
513
     */
514 543 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...
515
    {
516 543
        foreach ($this->documentUpserts as $document) {
517 73
            $class = $this->dm->getClassMetadata(get_class($document));
518 73
            if ( ! $class->isEmbeddedDocument) {
519 73
                $this->computeChangeSet($class, $document);
520 73
            }
521 543
        }
522 543
    }
523
524
    /**
525
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
526
     *
527
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
528
     * 2. Proxies are skipped.
529
     * 3. Only if document is properly managed.
530
     *
531
     * @param  object $document
532
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
533
     * @return void
534
     */
535 5
    private function computeSingleDocumentChangeSet($document)
536
    {
537 5
        $state = $this->getDocumentState($document);
538
539 5
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
540 1
            throw new \InvalidArgumentException('Document has to be managed or scheduled for removal for single computation ' . $this->objToStr($document));
541
        }
542
543 4
        $class = $this->dm->getClassMetadata(get_class($document));
544
545 4
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
546 3
            $this->persist($document);
547 3
        }
548
549
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
550 4
        $this->computeScheduleInsertsChangeSets();
551 4
        $this->computeScheduleUpsertsChangeSets();
552
553
        // Ignore uninitialized proxy objects
554 4
        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...
555
            return;
556
        }
557
558
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
559 4
        $oid = spl_object_hash($document);
560
561 4 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...
562 4
            && ! isset($this->documentUpserts[$oid])
563 4
            && ! isset($this->documentDeletions[$oid])
564 4
            && isset($this->documentStates[$oid])
565 4
        ) {
566
            $this->computeChangeSet($class, $document);
567
        }
568 4
    }
569
570
    /**
571
     * Gets the changeset for a document.
572
     *
573
     * @param object $document
574
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
575
     */
576 4
    public function getDocumentChangeSet($document)
577
    {
578 4
        $oid = spl_object_hash($document);
579 4
        if (isset($this->documentChangeSets[$oid])) {
580 4
            return $this->documentChangeSets[$oid];
581
        }
582
        return array();
583
    }
584
585
    /**
586
     * INTERNAL:
587
     * Sets the changeset for a document.
588
     *
589
     * @param object $document
590
     * @param array $changeset
591
     */
592
    public function setDocumentChangeSet($document, $changeset)
593
    {
594
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
595
    }
596
597
    /**
598
     * Get a documents actual data, flattening all the objects to arrays.
599
     *
600
     * @param object $document
601
     * @return array
602
     */
603 543
    public function getDocumentActualData($document)
604
    {
605 543
        $class = $this->dm->getClassMetadata(get_class($document));
606 543
        $actualData = array();
607 543
        foreach ($class->reflFields as $name => $refProp) {
608 543
            $mapping = $class->fieldMappings[$name];
609
            // skip not saved fields
610 543
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
611 50
                continue;
612
            }
613 543
            $value = $refProp->getValue($document);
614 543
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
615 6
                $value = new GridFSFile($value);
616 6
                $class->reflFields[$name]->setValue($document, $value);
617 6
                $actualData[$name] = $value;
618 543
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
619 543
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
620
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
621 381
                if ( ! $value instanceof Collection) {
622 118
                    $value = new ArrayCollection($value);
623 118
                }
624
625
                // Inject PersistentCollection
626 381
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
627 381
                $coll->setOwner($document, $mapping);
628 381
                $coll->setDirty( ! $value->isEmpty());
629 381
                $class->reflFields[$name]->setValue($document, $coll);
630 381
                $actualData[$name] = $coll;
631 381
            } else {
632 543
                $actualData[$name] = $value;
633
            }
634 543
        }
635 543
        return $actualData;
636
    }
637
638
    /**
639
     * Computes the changes that happened to a single document.
640
     *
641
     * Modifies/populates the following properties:
642
     *
643
     * {@link originalDocumentData}
644
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
645
     * then it was not fetched from the database and therefore we have no original
646
     * document data yet. All of the current document data is stored as the original document data.
647
     *
648
     * {@link documentChangeSets}
649
     * The changes detected on all properties of the document are stored there.
650
     * A change is a tuple array where the first entry is the old value and the second
651
     * entry is the new value of the property. Changesets are used by persisters
652
     * to INSERT/UPDATE the persistent document state.
653
     *
654
     * {@link documentUpdates}
655
     * If the document is already fully MANAGED (has been fetched from the database before)
656
     * and any changes to its properties are detected, then a reference to the document is stored
657
     * there to mark it for an update.
658
     *
659
     * @param ClassMetadata $class The class descriptor of the document.
660
     * @param object $document The document for which to compute the changes.
661
     */
662 543
    public function computeChangeSet(ClassMetadata $class, $document)
663
    {
664 543
        if ( ! $class->isInheritanceTypeNone()) {
665 177
            $class = $this->dm->getClassMetadata(get_class($document));
666 177
        }
667
668
        // Fire PreFlush lifecycle callbacks
669 543 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...
670 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
671 11
        }
672
673 543
        $this->computeOrRecomputeChangeSet($class, $document);
674 542
    }
675
676
    /**
677
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
678
     *
679
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
680
     * @param object $document
681
     * @param boolean $recompute
682
     */
683 543
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
684
    {
685 543
        $oid = spl_object_hash($document);
686 543
        $actualData = $this->getDocumentActualData($document);
687 543
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
688 543
        if ($isNewDocument) {
689
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
690
            // These result in an INSERT.
691 543
            $this->originalDocumentData[$oid] = $actualData;
692 543
            $changeSet = array();
693 543
            foreach ($actualData as $propName => $actualValue) {
694
                /* At this PersistentCollection shouldn't be here, probably it
695
                 * was cloned and its ownership must be fixed
696
                 */
697 543
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
698
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
699
                    $actualValue = $actualData[$propName];
700
                }
701
                // ignore inverse side of reference relationship
702 543 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...
703 184
                    continue;
704
                }
705 543
                $changeSet[$propName] = array(null, $actualValue);
706 543
            }
707 543
            $this->documentChangeSets[$oid] = $changeSet;
708 543
        } else {
709
            // Document is "fully" MANAGED: it was already fully persisted before
710
            // and we have a copy of the original data
711 8
            $originalData = $this->originalDocumentData[$oid];
712 8
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
713 8
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
714
                $changeSet = $this->documentChangeSets[$oid];
715
            } else {
716 8
                $changeSet = array();
717
            }
718
719 8
            foreach ($actualData as $propName => $actualValue) {
720
                // skip not saved fields
721 8
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
722
                    continue;
723
                }
724
725 8
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
726
727
                // skip if value has not changed
728 8
                if ($orgValue === $actualValue) {
729 8
                    if ($actualValue instanceof PersistentCollectionInterface) {
730 2
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
731
                            // consider dirty collections as changed as well
732 2
                            continue;
733
                        }
734 8
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
735
                        // but consider dirty GridFSFile instances as changed
736 8
                        continue;
737
                    }
738
                }
739
740
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
741 4
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
742
                    if ($orgValue !== null) {
743
                        $this->scheduleOrphanRemoval($orgValue);
744
                    }
745
746
                    $changeSet[$propName] = array($orgValue, $actualValue);
747
                    continue;
748
                }
749
750
                // if owning side of reference-one relationship
751 4
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
752
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
753
                        $this->scheduleOrphanRemoval($orgValue);
754
                    }
755
756
                    $changeSet[$propName] = array($orgValue, $actualValue);
757
                    continue;
758
                }
759
760 4
                if ($isChangeTrackingNotify) {
761
                    continue;
762
                }
763
764
                // ignore inverse side of reference relationship
765 4 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...
766
                    continue;
767
                }
768
769
                // Persistent collection was exchanged with the "originally"
770
                // created one. This can only mean it was cloned and replaced
771
                // on another document.
772 4
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
773
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
774
                }
775
776
                // if embed-many or reference-many relationship
777 4
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
778
                    $changeSet[$propName] = array($orgValue, $actualValue);
779
                    /* If original collection was exchanged with a non-empty value
780
                     * and $set will be issued, there is no need to $unset it first
781
                     */
782
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
783
                        continue;
784
                    }
785
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
786
                        $this->scheduleCollectionDeletion($orgValue);
787
                    }
788
                    continue;
789
                }
790
791
                // skip equivalent date values
792 4
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
793
                    $dateType = Type::getType('date');
794
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
795
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
796
797
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
798
                        continue;
799
                    }
800
                }
801
802
                // regular field
803 4
                $changeSet[$propName] = array($orgValue, $actualValue);
804 8
            }
805 8
            if ($changeSet) {
806 4
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
807 4
                    ? $changeSet + $this->documentChangeSets[$oid]
808 4
                    : $changeSet;
809
810 4
                $this->originalDocumentData[$oid] = $actualData;
811 4
                $this->scheduleForUpdate($document);
812 4
            }
813
        }
814
815
        // Look for changes in associations of the document
816 543
        $associationMappings = array_filter(
817 543
            $class->associationMappings,
818
            function ($assoc) { return empty($assoc['notSaved']); }
819 543
        );
820
821 543
        foreach ($associationMappings as $mapping) {
822 436
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
823
824 436
            if ($value === null) {
825 293
                continue;
826
            }
827
828 412
            $this->computeAssociationChanges($document, $mapping, $value);
829
830 411
            if (isset($mapping['reference'])) {
831 313
                continue;
832
            }
833
834 322
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
835
836 322
            foreach ($values as $obj) {
837 154
                $oid2 = spl_object_hash($obj);
838
839 154
                if (isset($this->documentChangeSets[$oid2])) {
840 152
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
841
842 152
                    if ( ! $isNewDocument) {
843 1
                        $this->scheduleForUpdate($document);
844 1
                    }
845
846 152
                    break;
847
                }
848 322
            }
849 542
        }
850 542
    }
851
852
    /**
853
     * Computes all the changes that have been done to documents and collections
854
     * since the last commit and stores these changes in the _documentChangeSet map
855
     * temporarily for access by the persisters, until the UoW commit is finished.
856
     */
857 540
    public function computeChangeSets()
858
    {
859 540
        $this->computeScheduleInsertsChangeSets();
860 539
        $this->computeScheduleUpsertsChangeSets();
861
862
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
863 539
        foreach ($this->identityMap as $className => $documents) {
864 539
            $class = $this->dm->getClassMetadata($className);
865 539
            if ($class->isEmbeddedDocument) {
866
                /* we do not want to compute changes to embedded documents up front
867
                 * in case embedded document was replaced and its changeset
868
                 * would corrupt data. Embedded documents' change set will
869
                 * be calculated by reachability from owning document.
870
                 */
871 150
                continue;
872
            }
873
874
            // If change tracking is explicit or happens through notification, then only compute
875
            // changes on document of that type that are explicitly marked for synchronization.
876 539
            switch (true) {
877 539
                case ($class->isChangeTrackingDeferredImplicit()):
878 538
                    $documentsToProcess = $documents;
879 538
                    break;
880
881 3
                case (isset($this->scheduledForDirtyCheck[$className])):
882
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
883
                    break;
884
885 3
                default:
886 3
                    $documentsToProcess = array();
887
888 3
            }
889
890 539
            foreach ($documentsToProcess as $document) {
891
                // Ignore uninitialized proxy objects
892 537
                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...
893
                    continue;
894
                }
895
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
896 537
                $oid = spl_object_hash($document);
897 537 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...
898 537
                    && ! isset($this->documentUpserts[$oid])
899 537
                    && ! isset($this->documentDeletions[$oid])
900 537
                    && isset($this->documentStates[$oid])
901 537
                ) {
902
                    $this->computeChangeSet($class, $document);
903
                }
904 539
            }
905 539
        }
906 539
    }
907
908
    /**
909
     * Computes the changes of an association.
910
     *
911
     * @param object $parentDocument
912
     * @param array $assoc
913
     * @param mixed $value The value of the association.
914
     * @throws \InvalidArgumentException
915
     */
916 412
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
917
    {
918 412
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
919 412
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
920 412
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
921
922 412
        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...
923
            return;
924
        }
925
926 412
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
927 210
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
928 205
                $this->scheduleCollectionUpdate($value);
929 205
            }
930 210
            $topmostOwner = $this->getOwningDocument($value->getOwner());
931 210
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
932 210
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
933 120
                $value->initialize();
934 120
                foreach ($value->getDeletedDocuments() as $orphan) {
935
                    $this->scheduleOrphanRemoval($orphan);
936 120
                }
937 120
            }
938 210
        }
939
940
        // Look through the documents, and in any of their associations,
941
        // for transient (new) documents, recursively. ("Persistence by reachability")
942
        // Unwrap. Uninitialized collections will simply be empty.
943 412
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
944
945 412
        $count = 0;
946 412
        foreach ($unwrappedValue as $key => $entry) {
947 293
            if ( ! is_object($entry)) {
948 1
                throw new \InvalidArgumentException(
949 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
950 1
                );
951
            }
952
953 292
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
954
955 292
            $state = $this->getDocumentState($entry, self::STATE_NEW);
956
957
            // Handle "set" strategy for multi-level hierarchy
958 292
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
959 292
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
960
961 292
            $count++;
962
963
            switch ($state) {
964 292
                case self::STATE_NEW:
965 3
                    if ( ! $assoc['isCascadePersist']) {
966
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
967
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
968
                            . ' Explicitly persist the new document or configure cascading persist operations'
969
                            . ' on the relationship.');
970
                    }
971
972 3
                    $this->persistNew($targetClass, $entry);
973 3
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
974 3
                    $this->computeChangeSet($targetClass, $entry);
975 3
                    break;
976
977 290
                case self::STATE_MANAGED:
978 290
                    if ($targetClass->isEmbeddedDocument) {
979 148
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
980 148
                        if ($knownParent && $knownParent !== $parentDocument) {
981 3
                            $entry = clone $entry;
982 3
                            if ($assoc['type'] === ClassMetadata::ONE) {
983 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
984 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
985 3
                            } else {
986
                                // must use unwrapped value to not trigger orphan removal
987 3
                                $unwrappedValue[$key] = $entry;
988
                            }
989 3
                            $this->persistNew($targetClass, $entry);
990 3
                        }
991 148
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
992 148
                        $this->computeChangeSet($targetClass, $entry);
993 148
                    }
994 290
                    break;
995
996
                case self::STATE_REMOVED:
997
                    // Consume the $value as array (it's either an array or an ArrayAccess)
998
                    // and remove the element from Collection.
999
                    if ($assoc['type'] === ClassMetadata::MANY) {
1000
                        unset($value[$key]);
1001
                    }
1002
                    break;
1003
1004
                case self::STATE_DETACHED:
1005
                    // Can actually not happen right now as we assume STATE_NEW,
1006
                    // so the exception will be raised from the DBAL layer (constraint violation).
1007
                    throw new \InvalidArgumentException('A detached document was found through a '
1008
                        . 'relationship during cascading a persist operation.');
1009
1010
                default:
1011
                    // MANAGED associated documents are already taken into account
1012
                    // during changeset calculation anyway, since they are in the identity map.
1013
1014
            }
1015 411
        }
1016 411
    }
1017
1018
    /**
1019
     * INTERNAL:
1020
     * Computes the changeset of an individual document, independently of the
1021
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1022
     *
1023
     * The passed document must be a managed document. If the document already has a change set
1024
     * because this method is invoked during a commit cycle then the change sets are added.
1025
     * whereby changes detected in this method prevail.
1026
     *
1027
     * @ignore
1028
     * @param ClassMetadata $class The class descriptor of the document.
1029
     * @param object $document The document for which to (re)calculate the change set.
1030
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1031
     */
1032 4
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1033
    {
1034
        // Ignore uninitialized proxy objects
1035 4
        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...
1036
            return;
1037
        }
1038
1039 4
        $oid = spl_object_hash($document);
1040
1041 4
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1042
            throw new \InvalidArgumentException('Document must be managed.');
1043
        }
1044
1045 4
        if ( ! $class->isInheritanceTypeNone()) {
1046
            $class = $this->dm->getClassMetadata(get_class($document));
1047
        }
1048
1049 4
        $this->computeOrRecomputeChangeSet($class, $document, true);
1050 4
    }
1051
1052
    /**
1053
     * @param ClassMetadata $class
1054
     * @param object $document
1055
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1056
     */
1057 571
    private function persistNew(ClassMetadata $class, $document)
1058
    {
1059 571
        $this->lifecycleEventManager->prePersist($class, $document);
1060 571
        $oid = spl_object_hash($document);
1061 571
        $upsert = false;
1062 571
        if ($class->identifier) {
1063 571
            $idValue = $class->getIdentifierValue($document);
1064 571
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1065
1066 571
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1067 3
                throw new \InvalidArgumentException(sprintf(
1068 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1069 3
                    get_class($document)
1070 3
                ));
1071
            }
1072
1073 570
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1074 1
                throw new \InvalidArgumentException(sprintf(
1075 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.',
1076 1
                    get_class($document)
1077 1
                ));
1078
            }
1079
1080 569
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1081 504
                $idValue = $class->idGenerator->generate($this->dm, $document);
1082 497
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1083 497
                $class->setIdentifierValue($document, $idValue);
1084 497
            }
1085
1086 562
            $this->documentIdentifiers[$oid] = $idValue;
1087 562
        } else {
1088
            // this is for embedded documents without identifiers
1089 131
            $this->documentIdentifiers[$oid] = $oid;
1090
        }
1091
1092 562
        $this->documentStates[$oid] = self::STATE_MANAGED;
1093
1094 562
        if ($upsert) {
1095 73
            $this->scheduleForUpsert($class, $document);
1096 73
        } else {
1097 502
            $this->scheduleForInsert($class, $document);
1098
        }
1099 562
    }
1100
1101
    /**
1102
     * Executes all document insertions for documents of the specified type.
1103
     *
1104
     * @param ClassMetadata $class
1105
     * @param array $documents Array of documents to insert
1106
     * @param array $options Array of options to be used with batchInsert()
1107
     */
1108 465 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...
1109
    {
1110 465
        $persister = $this->getDocumentPersister($class->name);
1111
1112
        foreach ($documents as $oid => $document) {
1113
            $persister->addInsert($document);
1114
            unset($this->documentInsertions[$oid]);
1115
        }
1116
1117
        $persister->executeInserts($options);
1118
1119
        foreach ($documents as $document) {
1120
            $this->lifecycleEventManager->postPersist($class, $document);
1121
        }
1122
    }
1123
1124
    /**
1125
     * Executes all document upserts for documents of the specified type.
1126
     *
1127
     * @param ClassMetadata $class
1128
     * @param array $documents Array of documents to upsert
1129
     * @param array $options Array of options to be used with batchInsert()
1130
     */
1131 73 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...
1132
    {
1133 73
        $persister = $this->getDocumentPersister($class->name);
1134
1135
1136
        foreach ($documents as $oid => $document) {
1137
            $persister->addUpsert($document);
1138
            unset($this->documentUpserts[$oid]);
1139
        }
1140
1141
        $persister->executeUpserts($options);
1142
1143
        foreach ($documents as $document) {
1144
            $this->lifecycleEventManager->postPersist($class, $document);
1145
        }
1146
    }
1147
1148
    /**
1149
     * Executes all document updates for documents of the specified type.
1150
     *
1151
     * @param Mapping\ClassMetadata $class
1152
     * @param array $documents Array of documents to update
1153
     * @param array $options Array of options to be used with update()
1154
     */
1155
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1156
    {
1157
        $className = $class->name;
1158
        $persister = $this->getDocumentPersister($className);
1159
1160
        foreach ($documents as $oid => $document) {
1161
            $this->lifecycleEventManager->preUpdate($class, $document);
1162
1163
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1164
                $persister->update($document, $options);
1165
            }
1166
1167
            unset($this->documentUpdates[$oid]);
1168
1169
            $this->lifecycleEventManager->postUpdate($class, $document);
1170
        }
1171
    }
1172
1173
    /**
1174
     * Executes all document deletions for documents of the specified type.
1175
     *
1176
     * @param ClassMetadata $class
1177
     * @param array $documents Array of documents to delete
1178
     * @param array $options Array of options to be used with remove()
1179
     */
1180
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1181
    {
1182
        $persister = $this->getDocumentPersister($class->name);
1183
1184
        foreach ($documents as $oid => $document) {
1185
            if ( ! $class->isEmbeddedDocument) {
1186
                $persister->delete($document, $options);
1187
            }
1188
            unset(
1189
                $this->documentDeletions[$oid],
1190
                $this->documentIdentifiers[$oid],
1191
                $this->originalDocumentData[$oid]
1192
            );
1193
1194
            // Clear snapshot information for any referenced PersistentCollection
1195
            // http://www.doctrine-project.org/jira/browse/MODM-95
1196
            foreach ($class->associationMappings as $fieldMapping) {
1197
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1198
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1199
                    if ($value instanceof PersistentCollectionInterface) {
1200
                        $value->clearSnapshot();
1201
                    }
1202
                }
1203
            }
1204
1205
            // Document with this $oid after deletion treated as NEW, even if the $oid
1206
            // is obtained by a new document because the old one went out of scope.
1207
            $this->documentStates[$oid] = self::STATE_NEW;
1208
1209
            $this->lifecycleEventManager->postRemove($class, $document);
1210
        }
1211
    }
1212
1213
    /**
1214
     * Schedules a document for insertion into the database.
1215
     * If the document already has an identifier, it will be added to the
1216
     * identity map.
1217
     *
1218
     * @param ClassMetadata $class
1219
     * @param object $document The document to schedule for insertion.
1220
     * @throws \InvalidArgumentException
1221
     */
1222 505
    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...
1223
    {
1224 505
        $oid = spl_object_hash($document);
1225
1226 505
        if (isset($this->documentUpdates[$oid])) {
1227
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1228
        }
1229 505
        if (isset($this->documentDeletions[$oid])) {
1230
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1231
        }
1232 505
        if (isset($this->documentInsertions[$oid])) {
1233
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1234
        }
1235
1236 505
        $this->documentInsertions[$oid] = $document;
1237
1238 505
        if (isset($this->documentIdentifiers[$oid])) {
1239 502
            $this->addToIdentityMap($document);
1240 502
        }
1241 505
    }
1242
1243
    /**
1244
     * Schedules a document for upsert into the database and adds it to the
1245
     * identity map
1246
     *
1247
     * @param ClassMetadata $class
1248
     * @param object $document The document to schedule for upsert.
1249
     * @throws \InvalidArgumentException
1250
     */
1251 76
    public function scheduleForUpsert(ClassMetadata $class, $document)
1252
    {
1253 76
        $oid = spl_object_hash($document);
1254
1255 76
        if ($class->isEmbeddedDocument) {
1256
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1257
        }
1258 76
        if (isset($this->documentUpdates[$oid])) {
1259
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1260
        }
1261 76
        if (isset($this->documentDeletions[$oid])) {
1262
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1263
        }
1264 76
        if (isset($this->documentUpserts[$oid])) {
1265
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1266
        }
1267
1268 76
        $this->documentUpserts[$oid] = $document;
1269 76
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1270 76
        $this->addToIdentityMap($document);
1271 76
    }
1272
1273
    /**
1274
     * Checks whether a document is scheduled for insertion.
1275
     *
1276
     * @param object $document
1277
     * @return boolean
1278
     */
1279 9
    public function isScheduledForInsert($document)
1280
    {
1281 9
        return isset($this->documentInsertions[spl_object_hash($document)]);
1282
    }
1283
1284
    /**
1285
     * Checks whether a document is scheduled for upsert.
1286
     *
1287
     * @param object $document
1288
     * @return boolean
1289
     */
1290 5
    public function isScheduledForUpsert($document)
1291
    {
1292 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1293
    }
1294
1295
    /**
1296
     * Schedules a document for being updated.
1297
     *
1298
     * @param object $document The document to schedule for being updated.
1299
     * @throws \InvalidArgumentException
1300
     */
1301 5
    public function scheduleForUpdate($document)
1302
    {
1303 5
        $oid = spl_object_hash($document);
1304 5
        if ( ! isset($this->documentIdentifiers[$oid])) {
1305
            throw new \InvalidArgumentException('Document has no identity.');
1306
        }
1307
1308 5
        if (isset($this->documentDeletions[$oid])) {
1309
            throw new \InvalidArgumentException('Document is removed.');
1310
        }
1311
1312 5
        if ( ! isset($this->documentUpdates[$oid])
1313 5
            && ! isset($this->documentInsertions[$oid])
1314 5
            && ! isset($this->documentUpserts[$oid])) {
1315
            $this->documentUpdates[$oid] = $document;
1316
        }
1317 5
    }
1318
1319
    /**
1320
     * Checks whether a document is registered as dirty in the unit of work.
1321
     * Note: Is not very useful currently as dirty documents are only registered
1322
     * at commit time.
1323
     *
1324
     * @param object $document
1325
     * @return boolean
1326
     */
1327 2
    public function isScheduledForUpdate($document)
1328
    {
1329 2
        return isset($this->documentUpdates[spl_object_hash($document)]);
1330
    }
1331
1332
    public function isScheduledForDirtyCheck($document)
1333
    {
1334
        $class = $this->dm->getClassMetadata(get_class($document));
1335
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1336
    }
1337
1338
    /**
1339
     * INTERNAL:
1340
     * Schedules a document for deletion.
1341
     *
1342
     * @param object $document
1343
     */
1344 2
    public function scheduleForDelete($document)
1345
    {
1346 2
        $oid = spl_object_hash($document);
1347
1348 2
        if (isset($this->documentInsertions[$oid])) {
1349 1
            if ($this->isInIdentityMap($document)) {
1350 1
                $this->removeFromIdentityMap($document);
1351 1
            }
1352 1
            unset($this->documentInsertions[$oid]);
1353 1
            return; // document has not been persisted yet, so nothing more to do.
1354
        }
1355
1356 1
        if ( ! $this->isInIdentityMap($document)) {
1357 1
            return; // ignore
1358
        }
1359
1360
        $this->removeFromIdentityMap($document);
1361
        $this->documentStates[$oid] = self::STATE_REMOVED;
1362
1363
        if (isset($this->documentUpdates[$oid])) {
1364
            unset($this->documentUpdates[$oid]);
1365
        }
1366
        if ( ! isset($this->documentDeletions[$oid])) {
1367
            $this->documentDeletions[$oid] = $document;
1368
        }
1369
    }
1370
1371
    /**
1372
     * Checks whether a document is registered as removed/deleted with the unit
1373
     * of work.
1374
     *
1375
     * @param object $document
1376
     * @return boolean
1377
     */
1378 2
    public function isScheduledForDelete($document)
1379
    {
1380 2
        return isset($this->documentDeletions[spl_object_hash($document)]);
1381
    }
1382
1383
    /**
1384
     * Checks whether a document is scheduled for insertion, update or deletion.
1385
     *
1386
     * @param $document
1387
     * @return boolean
1388
     */
1389 206
    public function isDocumentScheduled($document)
1390
    {
1391 206
        $oid = spl_object_hash($document);
1392 206
        return isset($this->documentInsertions[$oid]) ||
1393 9
            isset($this->documentUpserts[$oid]) ||
1394 1
            isset($this->documentUpdates[$oid]) ||
1395 206
            isset($this->documentDeletions[$oid]);
1396
    }
1397
1398
    /**
1399
     * INTERNAL:
1400
     * Registers a document in the identity map.
1401
     *
1402
     * Note that documents in a hierarchy are registered with the class name of
1403
     * the root document. Identifiers are serialized before being used as array
1404
     * keys to allow differentiation of equal, but not identical, values.
1405
     *
1406
     * @ignore
1407
     * @param object $document  The document to register.
1408
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1409
     *                  the document in question is already managed.
1410
     */
1411 569
    public function addToIdentityMap($document)
1412
    {
1413 569
        $class = $this->dm->getClassMetadata(get_class($document));
1414 569
        $id = $this->getIdForIdentityMap($document);
1415
1416 569
        if (isset($this->identityMap[$class->name][$id])) {
1417 5
            return false;
1418
        }
1419
1420 569
        $this->identityMap[$class->name][$id] = $document;
1421
1422 569
        if ($document instanceof NotifyPropertyChanged &&
1423 569
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1424 2
            $document->addPropertyChangedListener($this);
1425 2
        }
1426
1427 569
        return true;
1428
    }
1429
1430
    /**
1431
     * Gets the state of a document with regard to the current unit of work.
1432
     *
1433
     * @param object   $document
1434
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1435
     *                         This parameter can be set to improve performance of document state detection
1436
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1437
     *                         is either known or does not matter for the caller of the method.
1438
     * @return int The document state.
1439
     */
1440 573
    public function getDocumentState($document, $assume = null)
1441
    {
1442 573
        $oid = spl_object_hash($document);
1443
1444 573
        if (isset($this->documentStates[$oid])) {
1445 297
            return $this->documentStates[$oid];
1446
        }
1447
1448 573
        $class = $this->dm->getClassMetadata(get_class($document));
1449
1450 573
        if ($class->isEmbeddedDocument) {
1451 160
            return self::STATE_NEW;
1452
        }
1453
1454 570
        if ($assume !== null) {
1455 568
            return $assume;
1456
        }
1457
1458
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1459
         * known. Note that you cannot remember the NEW or DETACHED state in
1460
         * _documentStates since the UoW does not hold references to such
1461
         * objects and the object hash can be reused. More generally, because
1462
         * the state may "change" between NEW/DETACHED without the UoW being
1463
         * aware of it.
1464
         */
1465 2
        $id = $class->getIdentifierObject($document);
1466
1467 2
        if ($id === null) {
1468 2
            return self::STATE_NEW;
1469
        }
1470
1471
        // Check for a version field, if available, to avoid a DB lookup.
1472
        if ($class->isVersioned) {
1473
            return $class->getFieldValue($document, $class->versionField)
1474
                ? self::STATE_DETACHED
1475
                : self::STATE_NEW;
1476
        }
1477
1478
        // Last try before DB lookup: check the identity map.
1479
        if ($this->tryGetById($id, $class)) {
1480
            return self::STATE_DETACHED;
1481
        }
1482
1483
        // DB lookup
1484
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1485
            return self::STATE_DETACHED;
1486
        }
1487
1488
        return self::STATE_NEW;
1489
    }
1490
1491
    /**
1492
     * INTERNAL:
1493
     * Removes a document from the identity map. This effectively detaches the
1494
     * document from the persistence management of Doctrine.
1495
     *
1496
     * @ignore
1497
     * @param object $document
1498
     * @throws \InvalidArgumentException
1499
     * @return boolean
1500
     */
1501 3
    public function removeFromIdentityMap($document)
1502
    {
1503 3
        $oid = spl_object_hash($document);
1504
1505
        // Check if id is registered first
1506 3
        if ( ! isset($this->documentIdentifiers[$oid])) {
1507
            return false;
1508
        }
1509
1510 3
        $class = $this->dm->getClassMetadata(get_class($document));
1511 3
        $id = $this->getIdForIdentityMap($document);
1512
1513 3
        if (isset($this->identityMap[$class->name][$id])) {
1514 3
            unset($this->identityMap[$class->name][$id]);
1515 3
            $this->documentStates[$oid] = self::STATE_DETACHED;
1516 3
            return true;
1517
        }
1518
1519
        return false;
1520
    }
1521
1522
    /**
1523
     * INTERNAL:
1524
     * Gets a document in the identity map by its identifier hash.
1525
     *
1526
     * @ignore
1527
     * @param mixed         $id    Document identifier
1528
     * @param ClassMetadata $class Document class
1529
     * @return object
1530
     * @throws InvalidArgumentException if the class does not have an identifier
1531
     */
1532
    public function getById($id, ClassMetadata $class)
1533
    {
1534
        if ( ! $class->identifier) {
1535
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1536
        }
1537
1538
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1539
1540
        return $this->identityMap[$class->name][$serializedId];
1541
    }
1542
1543
    /**
1544
     * INTERNAL:
1545
     * Tries to get a document by its identifier hash. If no document is found
1546
     * for the given hash, FALSE is returned.
1547
     *
1548
     * @ignore
1549
     * @param mixed         $id    Document identifier
1550
     * @param ClassMetadata $class Document class
1551
     * @return mixed The found document or FALSE.
1552
     * @throws InvalidArgumentException if the class does not have an identifier
1553
     */
1554 6
    public function tryGetById($id, ClassMetadata $class)
1555
    {
1556 6
        if ( ! $class->identifier) {
1557
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1558
        }
1559
1560 6
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1561
1562 6
        return isset($this->identityMap[$class->name][$serializedId]) ?
1563 6
            $this->identityMap[$class->name][$serializedId] : false;
1564
    }
1565
1566
    /**
1567
     * Schedules a document for dirty-checking at commit-time.
1568
     *
1569
     * @param object $document The document to schedule for dirty-checking.
1570
     * @todo Rename: scheduleForSynchronization
1571
     */
1572
    public function scheduleForDirtyCheck($document)
1573
    {
1574
        $class = $this->dm->getClassMetadata(get_class($document));
1575
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1576
    }
1577
1578
    /**
1579
     * Checks whether a document is registered in the identity map.
1580
     *
1581
     * @param object $document
1582
     * @return boolean
1583
     */
1584 5
    public function isInIdentityMap($document)
1585
    {
1586 5
        $oid = spl_object_hash($document);
1587
1588 5
        if ( ! isset($this->documentIdentifiers[$oid])) {
1589 3
            return false;
1590
        }
1591
1592 3
        $class = $this->dm->getClassMetadata(get_class($document));
1593 3
        $id = $this->getIdForIdentityMap($document);
1594
1595 3
        return isset($this->identityMap[$class->name][$id]);
1596
    }
1597
1598
    /**
1599
     * @param object $document
1600
     * @return string
1601
     */
1602 569
    private function getIdForIdentityMap($document)
1603
    {
1604 569
        $class = $this->dm->getClassMetadata(get_class($document));
1605
1606 569
        if ( ! $class->identifier) {
1607 132
            $id = spl_object_hash($document);
1608 132
        } else {
1609 568
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1610 568
            $id = serialize($class->getDatabaseIdentifierValue($id));
1611
        }
1612
1613 569
        return $id;
1614
    }
1615
1616
    /**
1617
     * INTERNAL:
1618
     * Checks whether an identifier exists in the identity map.
1619
     *
1620
     * @ignore
1621
     * @param string $id
1622
     * @param string $rootClassName
1623
     * @return boolean
1624
     */
1625
    public function containsId($id, $rootClassName)
1626
    {
1627
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1628
    }
1629
1630
    /**
1631
     * Persists a document as part of the current unit of work.
1632
     *
1633
     * @param object $document The document to persist.
1634
     * @throws MongoDBException If trying to persist MappedSuperclass.
1635
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1636
     */
1637 569
    public function persist($document)
1638
    {
1639 569
        $class = $this->dm->getClassMetadata(get_class($document));
1640 569
        if ($class->isMappedSuperclass || $class->isAggregationResultDocument) {
1641 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1642
        }
1643 568
        $visited = array();
1644 568
        $this->doPersist($document, $visited);
1645 557
    }
1646
1647
    /**
1648
     * Saves a document as part of the current unit of work.
1649
     * This method is internally called during save() cascades as it tracks
1650
     * the already visited documents to prevent infinite recursions.
1651
     *
1652
     * NOTE: This method always considers documents that are not yet known to
1653
     * this UnitOfWork as NEW.
1654
     *
1655
     * @param object $document The document to persist.
1656
     * @param array $visited The already visited documents.
1657
     * @throws \InvalidArgumentException
1658
     * @throws MongoDBException
1659
     */
1660 568
    private function doPersist($document, array &$visited)
1661
    {
1662 568
        $oid = spl_object_hash($document);
1663 568
        if (isset($visited[$oid])) {
1664 23
            return; // Prevent infinite recursion
1665
        }
1666
1667 568
        $visited[$oid] = $document; // Mark visited
1668
1669 568
        $class = $this->dm->getClassMetadata(get_class($document));
1670
1671 568
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1672
        switch ($documentState) {
1673 568
            case self::STATE_MANAGED:
1674
                // Nothing to do, except if policy is "deferred explicit"
1675 32
                if ($class->isChangeTrackingDeferredExplicit()) {
1676
                    $this->scheduleForDirtyCheck($document);
1677
                }
1678 32
                break;
1679 568
            case self::STATE_NEW:
1680 568
                $this->persistNew($class, $document);
1681 559
                break;
1682
1683
            case self::STATE_REMOVED:
1684
                // Document becomes managed again
1685
                unset($this->documentDeletions[$oid]);
1686
1687
                $this->documentStates[$oid] = self::STATE_MANAGED;
1688
                break;
1689
1690
            case self::STATE_DETACHED:
1691
                throw new \InvalidArgumentException(
1692
                    'Behavior of persist() for a detached document is not yet defined.');
1693
1694
            default:
1695
                throw MongoDBException::invalidDocumentState($documentState);
1696
        }
1697
1698 559
        $this->cascadePersist($document, $visited);
1699 557
    }
1700
1701
    /**
1702
     * Deletes a document as part of the current unit of work.
1703
     *
1704
     * @param object $document The document to remove.
1705
     */
1706 1
    public function remove($document)
1707
    {
1708 1
        $visited = array();
1709 1
        $this->doRemove($document, $visited);
1710 1
    }
1711
1712
    /**
1713
     * Deletes a document as part of the current unit of work.
1714
     *
1715
     * This method is internally called during delete() cascades as it tracks
1716
     * the already visited documents to prevent infinite recursions.
1717
     *
1718
     * @param object $document The document to delete.
1719
     * @param array $visited The map of the already visited documents.
1720
     * @throws MongoDBException
1721
     */
1722 1
    private function doRemove($document, array &$visited)
1723
    {
1724 1
        $oid = spl_object_hash($document);
1725 1
        if (isset($visited[$oid])) {
1726
            return; // Prevent infinite recursion
1727
        }
1728
1729 1
        $visited[$oid] = $document; // mark visited
1730
1731
        /* Cascade first, because scheduleForDelete() removes the entity from
1732
         * the identity map, which can cause problems when a lazy Proxy has to
1733
         * be initialized for the cascade operation.
1734
         */
1735 1
        $this->cascadeRemove($document, $visited);
1736
1737 1
        $class = $this->dm->getClassMetadata(get_class($document));
1738 1
        $documentState = $this->getDocumentState($document);
1739
        switch ($documentState) {
1740 1
            case self::STATE_NEW:
1741 1
            case self::STATE_REMOVED:
1742
                // nothing to do
1743
                break;
1744 1
            case self::STATE_MANAGED:
1745 1
                $this->lifecycleEventManager->preRemove($class, $document);
1746 1
                $this->scheduleForDelete($document);
1747 1
                break;
1748
            case self::STATE_DETACHED:
1749
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1750
            default:
1751
                throw MongoDBException::invalidDocumentState($documentState);
1752
        }
1753 1
    }
1754
1755
    /**
1756
     * Merges the state of the given detached document into this UnitOfWork.
1757
     *
1758
     * @param object $document
1759
     * @return object The managed copy of the document.
1760
     */
1761 3
    public function merge($document)
1762
    {
1763 3
        $visited = array();
1764
1765 3
        return $this->doMerge($document, $visited);
1766
    }
1767
1768
    /**
1769
     * Executes a merge operation on a document.
1770
     *
1771
     * @param object      $document
1772
     * @param array       $visited
1773
     * @param object|null $prevManagedCopy
1774
     * @param array|null  $assoc
1775
     *
1776
     * @return object The managed copy of the document.
1777
     *
1778
     * @throws InvalidArgumentException If the entity instance is NEW.
1779
     * @throws LockException If the document uses optimistic locking through a
1780
     *                       version attribute and the version check against the
1781
     *                       managed copy fails.
1782
     */
1783 3
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1784
    {
1785 3
        $oid = spl_object_hash($document);
1786
1787 3
        if (isset($visited[$oid])) {
1788
            return $visited[$oid]; // Prevent infinite recursion
1789
        }
1790
1791 3
        $visited[$oid] = $document; // mark visited
1792
1793 3
        $class = $this->dm->getClassMetadata(get_class($document));
1794
1795
        /* First we assume DETACHED, although it can still be NEW but we can
1796
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1797
         * an identity, we need to fetch it from the DB anyway in order to
1798
         * merge. MANAGED documents are ignored by the merge operation.
1799
         */
1800 3
        $managedCopy = $document;
1801
1802 3
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1803 3
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1804
                $document->__load();
1805
            }
1806
1807
            // Try to look the document up in the identity map.
1808 3
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1809
1810 3
            if ($id === null) {
1811
                // If there is no identifier, it is actually NEW.
1812 3
                $managedCopy = $class->newInstance();
1813 3
                $this->persistNew($class, $managedCopy);
1814 3
            } else {
1815
                $managedCopy = $this->tryGetById($id, $class);
1816
1817
                if ($managedCopy) {
1818
                    // We have the document in memory already, just make sure it is not removed.
1819
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1820
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1821
                    }
1822
                } else {
1823
                    // We need to fetch the managed copy in order to merge.
1824
                    $managedCopy = $this->dm->find($class->name, $id);
1825
                }
1826
1827
                if ($managedCopy === null) {
1828
                    // If the identifier is ASSIGNED, it is NEW
1829
                    $managedCopy = $class->newInstance();
1830
                    $class->setIdentifierValue($managedCopy, $id);
1831
                    $this->persistNew($class, $managedCopy);
1832
                } else {
1833
                    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...
1834
                        $managedCopy->__load();
1835
                    }
1836
                }
1837
            }
1838
1839 3
            if ($class->isVersioned) {
1840
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1841
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1842
1843
                // Throw exception if versions don't match
1844
                if ($managedCopyVersion != $documentVersion) {
1845
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1846
                }
1847
            }
1848
1849
            // Merge state of $document into existing (managed) document
1850 3
            foreach ($class->reflClass->getProperties() as $prop) {
1851 3
                $name = $prop->name;
1852 3
                $prop->setAccessible(true);
1853 3
                if ( ! isset($class->associationMappings[$name])) {
1854 3
                    if ( ! $class->isIdentifier($name)) {
1855 3
                        $prop->setValue($managedCopy, $prop->getValue($document));
1856 3
                    }
1857 3
                } else {
1858 3
                    $assoc2 = $class->associationMappings[$name];
1859
1860 3
                    if ($assoc2['type'] === 'one') {
1861 1
                        $other = $prop->getValue($document);
1862
1863 1
                        if ($other === null) {
1864
                            $prop->setValue($managedCopy, null);
1865 1
                        } 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...
1866
                            // Do not merge fields marked lazy that have not been fetched
1867
                            continue;
1868 1
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1869
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1870
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1871
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1872
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1873
                                $relatedId = $targetClass->getIdentifierObject($other);
1874
1875
                                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...
1876
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1877
                                } else {
1878
                                    $other = $this
1879
                                        ->dm
1880
                                        ->getProxyFactory()
1881
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1882
                                    $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...
1883
                                }
1884
                            }
1885
1886
                            $prop->setValue($managedCopy, $other);
1887
                        }
1888 1
                    } else {
1889 2
                        $mergeCol = $prop->getValue($document);
1890
1891 2
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized()) {
1892
                            /* Do not merge fields marked lazy that have not
1893
                             * been fetched. Keep the lazy persistent collection
1894
                             * of the managed copy.
1895
                             */
1896
                            continue;
1897
                        }
1898
1899 2
                        $managedCol = $prop->getValue($managedCopy);
1900
1901 2
                        if ( ! $managedCol) {
1902 2
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1903 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1904 2
                            $prop->setValue($managedCopy, $managedCol);
1905 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1906 2
                        }
1907
1908
                        /* Note: do not process association's target documents.
1909
                         * They will be handled during the cascade. Initialize
1910
                         * and, if necessary, clear $managedCol for now.
1911
                         */
1912 2
                        if ($assoc2['isCascadeMerge']) {
1913 2
                            $managedCol->initialize();
1914
1915
                            // If $managedCol differs from the merged collection, clear and set dirty
1916 2
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1917
                                $managedCol->unwrap()->clear();
1918
                                $managedCol->setDirty(true);
1919
1920
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1921
                                    $this->scheduleForDirtyCheck($managedCopy);
1922
                                }
1923
                            }
1924 2
                        }
1925
                    }
1926
                }
1927
1928 3
                if ($class->isChangeTrackingNotify()) {
1929
                    // Just treat all properties as changed, there is no other choice.
1930
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1931
                }
1932 3
            }
1933
1934 3
            if ($class->isChangeTrackingDeferredExplicit()) {
1935
                $this->scheduleForDirtyCheck($document);
1936
            }
1937 3
        }
1938
1939 3
        if ($prevManagedCopy !== null) {
1940 3
            $assocField = $assoc['fieldName'];
1941 3
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1942
1943 3
            if ($assoc['type'] === 'one') {
1944 1
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1945 1
            } else {
1946 2
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1947
1948 2
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1949
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1950
                }
1951
            }
1952 3
        }
1953
1954
        // Mark the managed copy visited as well
1955 3
        $visited[spl_object_hash($managedCopy)] = true;
1956
1957 3
        $this->cascadeMerge($document, $managedCopy, $visited);
1958
1959 3
        return $managedCopy;
1960
    }
1961
1962
    /**
1963
     * Detaches a document from the persistence management. It's persistence will
1964
     * no longer be managed by Doctrine.
1965
     *
1966
     * @param object $document The document to detach.
1967
     */
1968 2
    public function detach($document)
1969
    {
1970 2
        $visited = array();
1971 2
        $this->doDetach($document, $visited);
1972 2
    }
1973
1974
    /**
1975
     * Executes a detach operation on the given document.
1976
     *
1977
     * @param object $document
1978
     * @param array $visited
1979
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1980
     */
1981 2
    private function doDetach($document, array &$visited)
1982
    {
1983 2
        $oid = spl_object_hash($document);
1984 2
        if (isset($visited[$oid])) {
1985
            return; // Prevent infinite recursion
1986
        }
1987
1988 2
        $visited[$oid] = $document; // mark visited
1989
1990 2
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1991 2
            case self::STATE_MANAGED:
1992 2
                $this->removeFromIdentityMap($document);
1993 2
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1994 2
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1995 2
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1996 2
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1997 2
                    $this->hasScheduledCollections[$oid]);
1998 2
                break;
1999
            case self::STATE_NEW:
2000
            case self::STATE_DETACHED:
2001
                return;
2002 2
        }
2003
2004 2
        $this->cascadeDetach($document, $visited);
2005 2
    }
2006
2007
    /**
2008
     * Refreshes the state of the given document from the database, overwriting
2009
     * any local, unpersisted changes.
2010
     *
2011
     * @param object $document The document to refresh.
2012
     * @throws \InvalidArgumentException If the document is not MANAGED.
2013
     */
2014
    public function refresh($document)
2015
    {
2016
        $visited = array();
2017
        $this->doRefresh($document, $visited);
2018
    }
2019
2020
    /**
2021
     * Executes a refresh operation on a document.
2022
     *
2023
     * @param object $document The document to refresh.
2024
     * @param array $visited The already visited documents during cascades.
2025
     * @throws \InvalidArgumentException If the document is not MANAGED.
2026
     */
2027
    private function doRefresh($document, array &$visited)
2028
    {
2029
        $oid = spl_object_hash($document);
2030
        if (isset($visited[$oid])) {
2031
            return; // Prevent infinite recursion
2032
        }
2033
2034
        $visited[$oid] = $document; // mark visited
2035
2036
        $class = $this->dm->getClassMetadata(get_class($document));
2037
2038
        if ( ! $class->isEmbeddedDocument) {
2039
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2040
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2041
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2042
            } else {
2043
                throw new \InvalidArgumentException('Document is not MANAGED.');
2044
            }
2045
        }
2046
2047
        $this->cascadeRefresh($document, $visited);
2048
    }
2049
2050
    /**
2051
     * Cascades a refresh operation to associated documents.
2052
     *
2053
     * @param object $document
2054
     * @param array $visited
2055
     */
2056
    private function cascadeRefresh($document, array &$visited)
2057
    {
2058
        $class = $this->dm->getClassMetadata(get_class($document));
2059
2060
        $associationMappings = array_filter(
2061
            $class->associationMappings,
2062
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2063
        );
2064
2065
        foreach ($associationMappings as $mapping) {
2066
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2067
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2068
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2069
                    // Unwrap so that foreach() does not initialize
2070
                    $relatedDocuments = $relatedDocuments->unwrap();
2071
                }
2072
                foreach ($relatedDocuments as $relatedDocument) {
2073
                    $this->doRefresh($relatedDocument, $visited);
2074
                }
2075
            } elseif ($relatedDocuments !== null) {
2076
                $this->doRefresh($relatedDocuments, $visited);
2077
            }
2078
        }
2079
    }
2080
2081
    /**
2082
     * Cascades a detach operation to associated documents.
2083
     *
2084
     * @param object $document
2085
     * @param array $visited
2086
     */
2087 2 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...
2088
    {
2089 2
        $class = $this->dm->getClassMetadata(get_class($document));
2090 2
        foreach ($class->fieldMappings as $mapping) {
2091 2
            if ( ! $mapping['isCascadeDetach']) {
2092 2
                continue;
2093
            }
2094 2
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2095 2
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2096 2
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2097
                    // Unwrap so that foreach() does not initialize
2098 1
                    $relatedDocuments = $relatedDocuments->unwrap();
2099 1
                }
2100 2
                foreach ($relatedDocuments as $relatedDocument) {
2101 1
                    $this->doDetach($relatedDocument, $visited);
2102 2
                }
2103 2
            } elseif ($relatedDocuments !== null) {
2104 1
                $this->doDetach($relatedDocuments, $visited);
2105 1
            }
2106 2
        }
2107 2
    }
2108
    /**
2109
     * Cascades a merge operation to associated documents.
2110
     *
2111
     * @param object $document
2112
     * @param object $managedCopy
2113
     * @param array $visited
2114
     */
2115 3
    private function cascadeMerge($document, $managedCopy, array &$visited)
2116
    {
2117 3
        $class = $this->dm->getClassMetadata(get_class($document));
2118
2119 3
        $associationMappings = array_filter(
2120 3
            $class->associationMappings,
2121
            function ($assoc) { return $assoc['isCascadeMerge']; }
2122 3
        );
2123
2124 3
        foreach ($associationMappings as $assoc) {
2125 3
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2126
2127 3
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2128 2
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2129
                    // Collections are the same, so there is nothing to do
2130
                    continue;
2131
                }
2132
2133 2
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2134
                    // Unwrap so that foreach() does not initialize
2135
                    $relatedDocuments = $relatedDocuments->unwrap();
2136
                }
2137
2138 2
                foreach ($relatedDocuments as $relatedDocument) {
2139 2
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2140 2
                }
2141 3
            } elseif ($relatedDocuments !== null) {
2142 1
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2143 1
            }
2144 3
        }
2145 3
    }
2146
2147
    /**
2148
     * Cascades the save operation to associated documents.
2149
     *
2150
     * @param object $document
2151
     * @param array $visited
2152
     */
2153 559
    private function cascadePersist($document, array &$visited)
2154
    {
2155 559
        $class = $this->dm->getClassMetadata(get_class($document));
2156
2157 559
        $associationMappings = array_filter(
2158 559
            $class->associationMappings,
2159
            function ($assoc) { return $assoc['isCascadePersist']; }
2160 559
        );
2161
2162 559
        foreach ($associationMappings as $fieldName => $mapping) {
2163 411
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2164
2165 411
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2166 353
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2167 1
                    if ($relatedDocuments->getOwner() !== $document) {
2168
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2169
                    }
2170
                    // Unwrap so that foreach() does not initialize
2171 1
                    $relatedDocuments = $relatedDocuments->unwrap();
2172 1
                }
2173
2174 353
                $count = 0;
2175 353
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2176 187
                    if ( ! empty($mapping['embedded'])) {
2177 112
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2178 112
                        if ($knownParent && $knownParent !== $document) {
2179 3
                            $relatedDocument = clone $relatedDocument;
2180 3
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2181 3
                        }
2182 112
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2183 112
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2184 112
                    }
2185 187
                    $this->doPersist($relatedDocument, $visited);
2186 352
                }
2187 411
            } elseif ($relatedDocuments !== null) {
2188 115
                if ( ! empty($mapping['embedded'])) {
2189 62
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2190 62
                    if ($knownParent && $knownParent !== $document) {
2191 5
                        $relatedDocuments = clone $relatedDocuments;
2192 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2193 5
                    }
2194 62
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2195 62
                }
2196 115
                $this->doPersist($relatedDocuments, $visited);
2197 114
            }
2198 558
        }
2199 557
    }
2200
2201
    /**
2202
     * Cascades the delete operation to associated documents.
2203
     *
2204
     * @param object $document
2205
     * @param array $visited
2206
     */
2207 1 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...
2208
    {
2209 1
        $class = $this->dm->getClassMetadata(get_class($document));
2210 1
        foreach ($class->fieldMappings as $mapping) {
2211 1
            if ( ! $mapping['isCascadeRemove']) {
2212 1
                continue;
2213
            }
2214
            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...
2215
                $document->__load();
2216
            }
2217
2218
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2219
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2220
                // If its a PersistentCollection initialization is intended! No unwrap!
2221
                foreach ($relatedDocuments as $relatedDocument) {
2222
                    $this->doRemove($relatedDocument, $visited);
2223
                }
2224
            } elseif ($relatedDocuments !== null) {
2225
                $this->doRemove($relatedDocuments, $visited);
2226
            }
2227 1
        }
2228 1
    }
2229
2230
    /**
2231
     * Acquire a lock on the given document.
2232
     *
2233
     * @param object $document
2234
     * @param int $lockMode
2235
     * @param int $lockVersion
2236
     * @throws LockException
2237
     * @throws \InvalidArgumentException
2238
     */
2239 1
    public function lock($document, $lockMode, $lockVersion = null)
2240
    {
2241 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2242 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2243
        }
2244
2245
        $documentName = get_class($document);
2246
        $class = $this->dm->getClassMetadata($documentName);
2247
2248
        if ($lockMode == LockMode::OPTIMISTIC) {
2249
            if ( ! $class->isVersioned) {
2250
                throw LockException::notVersioned($documentName);
2251
            }
2252
2253
            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...
2254
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2255
                if ($documentVersion != $lockVersion) {
2256
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2257
                }
2258
            }
2259
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2260
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2261
        }
2262
    }
2263
2264
    /**
2265
     * Releases a lock on the given document.
2266
     *
2267
     * @param object $document
2268
     * @throws \InvalidArgumentException
2269
     */
2270
    public function unlock($document)
2271
    {
2272
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2273
            throw new \InvalidArgumentException("Document is not MANAGED.");
2274
        }
2275
        $documentName = get_class($document);
2276
        $this->getDocumentPersister($documentName)->unlock($document);
2277
    }
2278
2279
    /**
2280
     * Clears the UnitOfWork.
2281
     *
2282
     * @param string|null $documentName if given, only documents of this type will get detached.
2283
     */
2284 7
    public function clear($documentName = null)
2285
    {
2286 7
        if ($documentName === null) {
2287 7
            $this->identityMap =
2288 7
            $this->documentIdentifiers =
2289 7
            $this->originalDocumentData =
2290 7
            $this->documentChangeSets =
2291 7
            $this->documentStates =
2292 7
            $this->scheduledForDirtyCheck =
2293 7
            $this->documentInsertions =
2294 7
            $this->documentUpserts =
2295 7
            $this->documentUpdates =
2296 7
            $this->documentDeletions =
2297 7
            $this->collectionUpdates =
2298 7
            $this->collectionDeletions =
2299 7
            $this->parentAssociations =
2300 7
            $this->orphanRemovals =
2301 7
            $this->hasScheduledCollections = array();
2302 7
        } else {
2303
            $visited = array();
2304
            foreach ($this->identityMap as $className => $documents) {
2305
                if ($className === $documentName) {
2306
                    foreach ($documents as $document) {
2307
                        $this->doDetach($document, $visited);
2308
                    }
2309
                }
2310
            }
2311
        }
2312
2313 7 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...
2314
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2315
        }
2316 7
    }
2317
2318
    /**
2319
     * INTERNAL:
2320
     * Schedules an embedded document for removal. The remove() operation will be
2321
     * invoked on that document at the beginning of the next commit of this
2322
     * UnitOfWork.
2323
     *
2324
     * @ignore
2325
     * @param object $document
2326
     */
2327
    public function scheduleOrphanRemoval($document)
2328
    {
2329
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2330
    }
2331
2332
    /**
2333
     * INTERNAL:
2334
     * Unschedules an embedded or referenced object for removal.
2335
     *
2336
     * @ignore
2337
     * @param object $document
2338
     */
2339 2
    public function unscheduleOrphanRemoval($document)
2340
    {
2341 2
        $oid = spl_object_hash($document);
2342 2
        if (isset($this->orphanRemovals[$oid])) {
2343
            unset($this->orphanRemovals[$oid]);
2344
        }
2345 2
    }
2346
2347
    /**
2348
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2349
     *  1) sets owner if it was cloned
2350
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2351
     *  3) NOP if state is OK
2352
     * Returned collection should be used from now on (only important with 2nd point)
2353
     *
2354
     * @param PersistentCollectionInterface $coll
2355
     * @param object $document
2356
     * @param ClassMetadata $class
2357
     * @param string $propName
2358
     * @return PersistentCollectionInterface
2359
     */
2360
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2361
    {
2362
        $owner = $coll->getOwner();
2363
        if ($owner === null) { // cloned
2364
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2365
        } elseif ($owner !== $document) { // no clone, we have to fix
2366
            if ( ! $coll->isInitialized()) {
2367
                $coll->initialize(); // we have to do this otherwise the cols share state
2368
            }
2369
            $newValue = clone $coll;
2370
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2371
            $class->reflFields[$propName]->setValue($document, $newValue);
2372
            if ($this->isScheduledForUpdate($document)) {
2373
                // @todo following line should be superfluous once collections are stored in change sets
2374
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2375
            }
2376
            return $newValue;
2377
        }
2378
        return $coll;
2379
    }
2380
2381
    /**
2382
     * INTERNAL:
2383
     * Schedules a complete collection for removal when this UnitOfWork commits.
2384
     *
2385
     * @param PersistentCollectionInterface $coll
2386
     */
2387
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2388
    {
2389
        $oid = spl_object_hash($coll);
2390
        unset($this->collectionUpdates[$oid]);
2391
        if ( ! isset($this->collectionDeletions[$oid])) {
2392
            $this->collectionDeletions[$oid] = $coll;
2393
            $this->scheduleCollectionOwner($coll);
2394
        }
2395
    }
2396
2397
    /**
2398
     * Checks whether a PersistentCollection is scheduled for deletion.
2399
     *
2400
     * @param PersistentCollectionInterface $coll
2401
     * @return boolean
2402
     */
2403 2
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2404
    {
2405 2
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2406
    }
2407
2408
    /**
2409
     * INTERNAL:
2410
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2411
     *
2412
     * @param PersistentCollectionInterface $coll
2413
     */
2414 37 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2672
            $oid = spl_object_hash($document);
2673
            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...
2674
                $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...
2675
                $overrideLocalValues = true;
2676
                if ($document instanceof NotifyPropertyChanged) {
2677
                    $document->addPropertyChangedListener($this);
2678
                }
2679
            } else {
2680
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2681
            }
2682
            if ($overrideLocalValues) {
2683
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2684
                $this->originalDocumentData[$oid] = $data;
2685
            }
2686
        } else {
2687
            if ($document === null) {
2688
                $document = $class->newInstance();
2689
            }
2690
2691
            if (! $class->isAggregationResultDocument) {
2692
                $this->registerManaged($document, $id, $data);
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

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