Completed
Pull Request — master (#1219)
by Maciej
13:47
created

UnitOfWork::tryGetById()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

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

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

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

Loading history...
382
            $options = array_merge($defaultOptions, $options);
383
        } else {
384 564
            $options = $defaultOptions;
385
        }
386
        // Compute changes done since last commit.
387 564
        if ($document === null) {
388 558
            $this->computeChangeSets();
389 563
        } elseif (is_object($document)) {
390 12
            $this->computeSingleDocumentChangeSet($document);
391 12
        } elseif (is_array($document)) {
392 1
            foreach ($document as $object) {
393 1
                $this->computeSingleDocumentChangeSet($object);
394 1
            }
395 1
        }
396
397 562
        if ( ! ($this->documentInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
409 47
            foreach ($this->orphanRemovals as $removal) {
410 47
                $this->remove($removal);
411 47
            }
412 47
        }
413
414
        // Raise onFlush
415 559
        if ($this->evm->hasListeners(Events::onFlush)) {
416 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
417 7
        }
418
419 559
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
420 78
            list($class, $documents) = $classAndDocuments;
421 78
            $this->executeUpserts($class, $documents, $options);
422 559
        }
423
424 559
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
425 492
            list($class, $documents) = $classAndDocuments;
426 492
            $this->executeInserts($class, $documents, $options);
427 558
        }
428
429 558
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
430 220
            list($class, $documents) = $classAndDocuments;
431 220
            $this->executeUpdates($class, $documents, $options);
432 558
        }
433
434 558
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
435 64
            list($class, $documents) = $classAndDocuments;
436 64
            $this->executeDeletions($class, $documents, $options);
437 558
        }
438
439
        // Raise postFlush
440 558
        if ($this->evm->hasListeners(Events::postFlush)) {
441
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
442
        }
443
444
        // Clear up
445 558
        $this->documentInsertions =
446 558
        $this->documentUpserts =
447 558
        $this->documentUpdates =
448 558
        $this->documentDeletions =
449 558
        $this->documentChangeSets =
450 558
        $this->collectionUpdates =
451 558
        $this->collectionDeletions =
452 558
        $this->visitedCollections =
453 558
        $this->scheduledForDirtyCheck =
454 558
        $this->orphanRemovals = 
455 558
        $this->hasScheduledCollections = array();
456 558
    }
457
458
    /**
459
     * Groups a list of scheduled documents by their class.
460
     *
461
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
462
     * @param bool $includeEmbedded
463
     * @return array Tuples of ClassMetadata and a corresponding array of objects
464
     */
465 559
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
466
    {
467 559
        if (empty($documents)) {
468 559
            return array();
469
        }
470 558
        $divided = array();
471 558
        $embeds = array();
472 558
        foreach ($documents as $oid => $d) {
473 558
            $className = get_class($d);
474 558
            if (isset($embeds[$className])) {
475 70
                continue;
476
            }
477 558
            if (isset($divided[$className])) {
478 138
                $divided[$className][1][$oid] = $d;
479 138
                continue;
480
            }
481 558
            $class = $this->dm->getClassMetadata($className);
482 558
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
483 169
                $embeds[$className] = true;
484 169
                continue;
485
            }
486 558
            if (empty($divided[$class->name])) {
487 558
                $divided[$class->name] = array($class, array($oid => $d));
488 558
            } else {
489 4
                $divided[$class->name][1][$oid] = $d;
490
            }
491 558
        }
492 558
        return $divided;
493
    }
494
495
    /**
496
     * Compute changesets of all documents scheduled for insertion.
497
     *
498
     * Embedded documents will not be processed.
499
     */
500 566 View Code Duplication
    private function computeScheduleInsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
501
    {
502 566
        foreach ($this->documentInsertions as $document) {
503 500
            $class = $this->dm->getClassMetadata(get_class($document));
504 500
            if ( ! $class->isEmbeddedDocument) {
505 497
                $this->computeChangeSet($class, $document);
506 496
            }
507 565
        }
508 565
    }
509
510
    /**
511
     * Compute changesets of all documents scheduled for upsert.
512
     *
513
     * Embedded documents will not be processed.
514
     */
515 565 View Code Duplication
    private function computeScheduleUpsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
516
    {
517 565
        foreach ($this->documentUpserts as $document) {
518 77
            $class = $this->dm->getClassMetadata(get_class($document));
519 77
            if ( ! $class->isEmbeddedDocument) {
520 77
                $this->computeChangeSet($class, $document);
521 77
            }
522 565
        }
523 565
    }
524
525
    /**
526
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
527
     *
528
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
529
     * 2. Proxies are skipped.
530
     * 3. Only if document is properly managed.
531
     *
532
     * @param  object $document
533
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
534
     * @return void
535
     */
536 13
    private function computeSingleDocumentChangeSet($document)
537
    {
538 13
        $state = $this->getDocumentState($document);
539
540 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
541 1
            throw new \InvalidArgumentException("Document has to be managed or scheduled for removal for single computation " . $this->objToStr($document));
542
        }
543
544 12
        $class = $this->dm->getClassMetadata(get_class($document));
545
546 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
547 9
            $this->persist($document);
548 9
        }
549
550
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
551 12
        $this->computeScheduleInsertsChangeSets();
552 12
        $this->computeScheduleUpsertsChangeSets();
553
554
        // Ignore uninitialized proxy objects
555 12
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
563 12
            && ! isset($this->documentUpserts[$oid])
564 12
            && ! isset($this->documentDeletions[$oid])
565 12
            && isset($this->documentStates[$oid])
566 12
        ) {
567 8
            $this->computeChangeSet($class, $document);
568 8
        }
569 12
    }
570
571
    /**
572
     * Gets the changeset for a document.
573
     *
574
     * @param object $document
575
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
576
     */
577 556
    public function getDocumentChangeSet($document)
578
    {
579 556
        $oid = spl_object_hash($document);
580 556
        if (isset($this->documentChangeSets[$oid])) {
581 556
            return $this->documentChangeSets[$oid];
582
        }
583 54
        return array();
584
    }
585
586
    /**
587
     * INTERNAL:
588
     * Sets the changeset for a document.
589
     *
590
     * @param object $document
591
     * @param array $changeset
592
     */
593 1
    public function setDocumentChangeSet($document, $changeset)
594
    {
595 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
596 1
    }
597
598
    /**
599
     * Get a documents actual data, flattening all the objects to arrays.
600
     *
601
     * @param object $document
602
     * @return array
603
     */
604 563
    public function getDocumentActualData($document)
605
    {
606 563
        $class = $this->dm->getClassMetadata(get_class($document));
607 563
        $actualData = array();
608 563
        foreach ($class->reflFields as $name => $refProp) {
609 563
            $mapping = $class->fieldMappings[$name];
610
            // skip not saved fields
611 563
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
612 50
                continue;
613
            }
614 563
            $value = $refProp->getValue($document);
615 563
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
616 5
                $value = new GridFSFile($value);
617 5
                $class->reflFields[$name]->setValue($document, $value);
618 5
                $actualData[$name] = $value;
619 563
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
620 563
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
621
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
622 370
                if ( ! $value instanceof Collection) {
623 119
                    $value = new ArrayCollection($value);
624 119
                }
625
626
                // Inject PersistentCollection
627 370
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
628 370
                $coll->setOwner($document, $mapping);
629 370
                $coll->setDirty( ! $value->isEmpty());
630 370
                $class->reflFields[$name]->setValue($document, $coll);
631 370
                $actualData[$name] = $coll;
632 370
            } else {
633 563
                $actualData[$name] = $value;
634
            }
635 563
        }
636 563
        return $actualData;
637
    }
638
639
    /**
640
     * Computes the changes that happened to a single document.
641
     *
642
     * Modifies/populates the following properties:
643
     *
644
     * {@link originalDocumentData}
645
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
646
     * then it was not fetched from the database and therefore we have no original
647
     * document data yet. All of the current document data is stored as the original document data.
648
     *
649
     * {@link documentChangeSets}
650
     * The changes detected on all properties of the document are stored there.
651
     * A change is a tuple array where the first entry is the old value and the second
652
     * entry is the new value of the property. Changesets are used by persisters
653
     * to INSERT/UPDATE the persistent document state.
654
     *
655
     * {@link documentUpdates}
656
     * If the document is already fully MANAGED (has been fetched from the database before)
657
     * and any changes to its properties are detected, then a reference to the document is stored
658
     * there to mark it for an update.
659
     *
660
     * @param ClassMetadata $class The class descriptor of the document.
661
     * @param object $document The document for which to compute the changes.
662
     */
663 563
    public function computeChangeSet(ClassMetadata $class, $document)
664
    {
665 563
        if ( ! $class->isInheritanceTypeNone()) {
666 172
            $class = $this->dm->getClassMetadata(get_class($document));
667 172
        }
668
669
        // Fire PreFlush lifecycle callbacks
670 563 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
671 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
672 11
        }
673
674 563
        $this->computeOrRecomputeChangeSet($class, $document);
675 562
    }
676
677
    /**
678
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
679
     *
680
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
681
     * @param object $document
682
     * @param boolean $recompute
683
     */
684 563
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
685
    {
686 563
        $oid = spl_object_hash($document);
687 563
        $actualData = $this->getDocumentActualData($document);
688 563
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
689 563
        if ($isNewDocument) {
690
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
691
            // These result in an INSERT.
692 563
            $this->originalDocumentData[$oid] = $actualData;
693 563
            $changeSet = array();
694 563
            foreach ($actualData as $propName => $actualValue) {
695
                /* At this PersistentCollection shouldn't be here, probably it
696
                 * was cloned and its ownership must be fixed
697
                 */
698 563
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
699
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
700
                    $actualValue = $actualData[$propName];
701
                }
702
                // ignore inverse side of reference relationship
703 563 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
704 175
                    continue;
705
                }
706 563
                $changeSet[$propName] = array(null, $actualValue);
707 563
            }
708 563
            $this->documentChangeSets[$oid] = $changeSet;
709 563
        } else {
710
            // Document is "fully" MANAGED: it was already fully persisted before
711
            // and we have a copy of the original data
712 280
            $originalData = $this->originalDocumentData[$oid];
713 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
714 280
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
715 2
                $changeSet = $this->documentChangeSets[$oid];
716 2
            } else {
717 280
                $changeSet = array();
718
            }
719
720 280
            foreach ($actualData as $propName => $actualValue) {
721
                // skip not saved fields
722 280
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
723
                    continue;
724
                }
725
726 280
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
727
728
                // skip if value has not changed
729 280
                if ($orgValue === $actualValue) {
730 279
                    if ($actualValue instanceof PersistentCollectionInterface) {
731 194
                        if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
732
                            // consider dirty collections as changed as well
733 171
                            continue;
734
                        }
735 279
                    } elseif ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
736
                        // but consider dirty GridFSFile instances as changed
737 279
                        continue;
738
                    }
739 94
                }
740
741
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
742 239
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
743 11
                    if ($orgValue !== null) {
744 6
                        $this->scheduleOrphanRemoval($orgValue);
745 6
                    }
746
747 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
748 11
                    continue;
749
                }
750
751
                // if owning side of reference-one relationship
752 232
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
753 11
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
754 1
                        $this->scheduleOrphanRemoval($orgValue);
755 1
                    }
756
757 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
758 11
                    continue;
759
                }
760
761 227
                if ($isChangeTrackingNotify) {
762 2
                    continue;
763
                }
764
765
                // ignore inverse side of reference relationship
766 226 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
767 6
                    continue;
768
                }
769
770
                // Persistent collection was exchanged with the "originally"
771
                // created one. This can only mean it was cloned and replaced
772
                // on another document.
773 224
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
774 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
775 6
                }
776
777
                // if embed-many or reference-many relationship
778 224
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
779 111
                    $changeSet[$propName] = array($orgValue, $actualValue);
780
                    /* If original collection was exchanged with a non-empty value
781
                     * and $set will be issued, there is no need to $unset it first
782
                     */
783 111
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
784 28
                        continue;
785
                    }
786 91
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
787 17
                        $this->scheduleCollectionDeletion($orgValue);
788 17
                    }
789 91
                    continue;
790
                }
791
792
                // skip equivalent date values
793 150
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
794 36
                    $dateType = Type::getType('date');
795 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
796 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
797
798 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
799 29
                        continue;
800
                    }
801 10
                }
802
803
                // regular field
804 134
                $changeSet[$propName] = array($orgValue, $actualValue);
805 280
            }
806 280
            if ($changeSet) {
807 229
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
808 229
                    ? $changeSet + $this->documentChangeSets[$oid]
809 21
                    : $changeSet;
810
811 229
                $this->originalDocumentData[$oid] = $actualData;
812 229
                $this->scheduleForUpdate($document);
813 229
            }
814
        }
815
816
        // Look for changes in associations of the document
817 563
        $associationMappings = array_filter(
818 563
            $class->associationMappings,
819
            function ($assoc) { return empty($assoc['notSaved']); }
820 563
        );
821
822 563
        foreach ($associationMappings as $mapping) {
823 434
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
824
825 434
            if ($value === null) {
826 290
                continue;
827
            }
828
829 425
            $this->computeAssociationChanges($document, $mapping, $value);
830
831 424
            if (isset($mapping['reference'])) {
832 319
                continue;
833
            }
834
835 332
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
836
837 332
            foreach ($values as $obj) {
838 173
                $oid2 = spl_object_hash($obj);
839
840 173
                if (isset($this->documentChangeSets[$oid2])) {
841 171
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
842
843 171
                    if ( ! $isNewDocument) {
844 77
                        $this->scheduleForUpdate($document);
845 77
                    }
846
847 171
                    break;
848
                }
849 332
            }
850 562
        }
851 562
    }
852
853
    /**
854
     * Computes all the changes that have been done to documents and collections
855
     * since the last commit and stores these changes in the _documentChangeSet map
856
     * temporarily for access by the persisters, until the UoW commit is finished.
857
     */
858 561
    public function computeChangeSets()
859
    {
860 561
        $this->computeScheduleInsertsChangeSets();
861 560
        $this->computeScheduleUpsertsChangeSets();
862
863
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
864 560
        foreach ($this->identityMap as $className => $documents) {
865 560
            $class = $this->dm->getClassMetadata($className);
866 560
            if ($class->isEmbeddedDocument) {
867
                /* we do not want to compute changes to embedded documents up front
868
                 * in case embedded document was replaced and its changeset
869
                 * would corrupt data. Embedded documents' change set will
870
                 * be calculated by reachability from owning document.
871
                 */
872 162
                continue;
873
            }
874
875
            // If change tracking is explicit or happens through notification, then only compute
876
            // changes on document of that type that are explicitly marked for synchronization.
877 560
            switch (true) {
878 560
                case ($class->isChangeTrackingDeferredImplicit()):
879 559
                    $documentsToProcess = $documents;
880 559
                    break;
881
882 3
                case (isset($this->scheduledForDirtyCheck[$className])):
883 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
884 2
                    break;
885
886 3
                default:
887 3
                    $documentsToProcess = array();
888
889 3
            }
890
891 560
            foreach ($documentsToProcess as $document) {
892
                // Ignore uninitialized proxy objects
893 556
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
894 10
                    continue;
895
                }
896
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
897 556
                $oid = spl_object_hash($document);
898 556 View Code Duplication
                if ( ! isset($this->documentInsertions[$oid])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
899 556
                    && ! isset($this->documentUpserts[$oid])
900 556
                    && ! isset($this->documentDeletions[$oid])
901 556
                    && isset($this->documentStates[$oid])
902 556
                ) {
903 265
                    $this->computeChangeSet($class, $document);
904 265
                }
905 560
            }
906 560
        }
907 560
    }
908
909
    /**
910
     * Computes the changes of an association.
911
     *
912
     * @param object $parentDocument
913
     * @param array $assoc
914
     * @param mixed $value The value of the association.
915
     * @throws \InvalidArgumentException
916
     */
917 425
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
918
    {
919 425
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
920 425
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
921 425
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
922
923 425
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
924 8
            return;
925
        }
926
927 424
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
928 230
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
929 226
                $this->scheduleCollectionUpdate($value);
930 226
            }
931 230
            $topmostOwner = $this->getOwningDocument($value->getOwner());
932 230
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
933 230
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
934 136
                $value->initialize();
935 136
                foreach ($value->getDeletedDocuments() as $orphan) {
936 22
                    $this->scheduleOrphanRemoval($orphan);
937 136
                }
938 136
            }
939 230
        }
940
941
        // Look through the documents, and in any of their associations,
942
        // for transient (new) documents, recursively. ("Persistence by reachability")
943
        // Unwrap. Uninitialized collections will simply be empty.
944 424
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
945
946 424
        $count = 0;
947 424
        foreach ($unwrappedValue as $key => $entry) {
948 329
            if ( ! is_object($entry)) {
949 1
                throw new \InvalidArgumentException(
950 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
951 1
                );
952
            }
953
954 328
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
955
956 328
            $state = $this->getDocumentState($entry, self::STATE_NEW);
957
958
            // Handle "set" strategy for multi-level hierarchy
959 328
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
960 328
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
961
962 328
            $count++;
963
964
            switch ($state) {
965 328
                case self::STATE_NEW:
966 58
                    if ( ! $assoc['isCascadePersist']) {
967
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
968
                            . " configured to cascade persist operations: " . $this->objToStr($entry) . "."
969
                            . " Explicitly persist the new document or configure cascading persist operations"
970
                            . " on the relationship.");
971
                    }
972
973 58
                    $this->persistNew($targetClass, $entry);
974 58
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
975 58
                    $this->computeChangeSet($targetClass, $entry);
976 58
                    break;
977
978 323
                case self::STATE_MANAGED:
979 323
                    if ($targetClass->isEmbeddedDocument) {
980 164
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
981 164
                        if ($knownParent && $knownParent !== $parentDocument) {
982 6
                            $entry = clone $entry;
983 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
984 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
985 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
986 3
                            } else {
987
                                // must use unwrapped value to not trigger orphan removal
988 6
                                $unwrappedValue[$key] = $entry;
989
                            }
990 6
                            $this->persistNew($targetClass, $entry);
991 6
                        }
992 164
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
993 164
                        $this->computeChangeSet($targetClass, $entry);
994 164
                    }
995 323
                    break;
996
997 1
                case self::STATE_REMOVED:
998
                    // Consume the $value as array (it's either an array or an ArrayAccess)
999
                    // and remove the element from Collection.
1000 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1001
                        unset($value[$key]);
1002
                    }
1003 1
                    break;
1004
1005
                case self::STATE_DETACHED:
1006
                    // Can actually not happen right now as we assume STATE_NEW,
1007
                    // so the exception will be raised from the DBAL layer (constraint violation).
1008
                    throw new \InvalidArgumentException("A detached document was found through a "
1009
                        . "relationship during cascading a persist operation.");
1010
1011
                default:
1012
                    // MANAGED associated documents are already taken into account
1013
                    // during changeset calculation anyway, since they are in the identity map.
1014
1015
            }
1016 423
        }
1017 423
    }
1018
1019
    /**
1020
     * INTERNAL:
1021
     * Computes the changeset of an individual document, independently of the
1022
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1023
     *
1024
     * The passed document must be a managed document. If the document already has a change set
1025
     * because this method is invoked during a commit cycle then the change sets are added.
1026
     * whereby changes detected in this method prevail.
1027
     *
1028
     * @ignore
1029
     * @param ClassMetadata $class The class descriptor of the document.
1030
     * @param object $document The document for which to (re)calculate the change set.
1031
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1032
     */
1033 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1034
    {
1035
        // Ignore uninitialized proxy objects
1036 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1037 1
            return;
1038
        }
1039
1040 19
        $oid = spl_object_hash($document);
1041
1042 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1043
            throw new \InvalidArgumentException('Document must be managed.');
1044
        }
1045
1046 19
        if ( ! $class->isInheritanceTypeNone()) {
1047 2
            $class = $this->dm->getClassMetadata(get_class($document));
1048 2
        }
1049
1050 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1051 19
    }
1052
1053
    /**
1054
     * @param ClassMetadata $class
1055
     * @param object $document
1056
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1057
     */
1058 587
    private function persistNew(ClassMetadata $class, $document)
1059
    {
1060 587
        $this->lifecycleEventManager->prePersist($class, $document);
1061 587
        $oid = spl_object_hash($document);
1062 587
        $upsert = false;
1063 587
        if ($class->identifier) {
1064 587
            $idValue = $class->getIdentifierValue($document);
1065 587
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1066
1067 587
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1068 3
                throw new \InvalidArgumentException(sprintf(
1069 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1070 3
                    get_class($document)
1071 3
                ));
1072
            }
1073
1074 586
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1075 1
                throw new \InvalidArgumentException(sprintf(
1076 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1077 1
                    get_class($document)
1078 1
                ));
1079
            }
1080
1081 585
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1082 514
                $idValue = $class->idGenerator->generate($this->dm, $document);
1083 514
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1084 514
                $class->setIdentifierValue($document, $idValue);
1085 514
            }
1086
1087 585
            $this->documentIdentifiers[$oid] = $idValue;
1088 585
        } else {
1089
            // this is for embedded documents without identifiers
1090 146
            $this->documentIdentifiers[$oid] = $oid;
1091
        }
1092
1093 585
        $this->documentStates[$oid] = self::STATE_MANAGED;
1094
1095 585
        if ($upsert) {
1096 81
            $this->scheduleForUpsert($class, $document);
1097 81
        } else {
1098 519
            $this->scheduleForInsert($class, $document);
1099
        }
1100 585
    }
1101
1102
    /**
1103
     * Executes all document insertions for documents of the specified type.
1104
     *
1105
     * @param ClassMetadata $class
1106
     * @param array $documents Array of documents to insert
1107
     * @param array $options Array of options to be used with batchInsert()
1108
     */
1109 492 View Code Duplication
    private function executeInserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1110
    {
1111 492
        $persister = $this->getDocumentPersister($class->name);
1112
1113 492
        foreach ($documents as $oid => $document) {
1114 492
            $persister->addInsert($document);
1115 492
            unset($this->documentInsertions[$oid]);
1116 492
        }
1117
1118 492
        $persister->executeInserts($options);
1119
1120 491
        foreach ($documents as $document) {
1121 491
            $this->lifecycleEventManager->postPersist($class, $document);
1122 491
        }
1123 491
    }
1124
1125
    /**
1126
     * Executes all document upserts for documents of the specified type.
1127
     *
1128
     * @param ClassMetadata $class
1129
     * @param array $documents Array of documents to upsert
1130
     * @param array $options Array of options to be used with batchInsert()
1131
     */
1132 78 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1133
    {
1134 78
        $persister = $this->getDocumentPersister($class->name);
1135
1136
1137 78
        foreach ($documents as $oid => $document) {
1138 78
            $persister->addUpsert($document);
1139 78
            unset($this->documentUpserts[$oid]);
1140 78
        }
1141
1142 78
        $persister->executeUpserts($options);
1143
1144 78
        foreach ($documents as $document) {
1145 78
            $this->lifecycleEventManager->postPersist($class, $document);
1146 78
        }
1147 78
    }
1148
1149
    /**
1150
     * Executes all document updates for documents of the specified type.
1151
     *
1152
     * @param Mapping\ClassMetadata $class
1153
     * @param array $documents Array of documents to update
1154
     * @param array $options Array of options to be used with update()
1155
     */
1156 220
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1157
    {
1158 220
        $className = $class->name;
1159 220
        $persister = $this->getDocumentPersister($className);
1160
1161 220
        foreach ($documents as $oid => $document) {
1162 220
            $this->lifecycleEventManager->preUpdate($class, $document);
1163
1164 220
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1165 218
                $persister->update($document, $options);
1166 214
            }
1167
1168 216
            unset($this->documentUpdates[$oid]);
1169
1170 216
            $this->lifecycleEventManager->postUpdate($class, $document);
1171 216
        }
1172 215
    }
1173
1174
    /**
1175
     * Executes all document deletions for documents of the specified type.
1176
     *
1177
     * @param ClassMetadata $class
1178
     * @param array $documents Array of documents to delete
1179
     * @param array $options Array of options to be used with remove()
1180
     */
1181 64
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1182
    {
1183 64
        $persister = $this->getDocumentPersister($class->name);
1184
1185 64
        foreach ($documents as $oid => $document) {
1186 64
            if ( ! $class->isEmbeddedDocument) {
1187 28
                $persister->delete($document, $options);
1188 26
            }
1189
            unset(
1190 62
                $this->documentDeletions[$oid],
1191 62
                $this->documentIdentifiers[$oid],
1192 62
                $this->originalDocumentData[$oid]
1193
            );
1194
1195
            // Clear snapshot information for any referenced PersistentCollection
1196
            // http://www.doctrine-project.org/jira/browse/MODM-95
1197 62
            foreach ($class->associationMappings as $fieldMapping) {
1198 42
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1199 27
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1200 27
                    if ($value instanceof PersistentCollectionInterface) {
1201 23
                        $value->clearSnapshot();
1202 23
                    }
1203 27
                }
1204 62
            }
1205
1206
            // Document with this $oid after deletion treated as NEW, even if the $oid
1207
            // is obtained by a new document because the old one went out of scope.
1208 62
            $this->documentStates[$oid] = self::STATE_NEW;
1209
1210 62
            $this->lifecycleEventManager->postRemove($class, $document);
1211 62
        }
1212 62
    }
1213
1214
    /**
1215
     * Schedules a document for insertion into the database.
1216
     * If the document already has an identifier, it will be added to the
1217
     * identity map.
1218
     *
1219
     * @param ClassMetadata $class
1220
     * @param object $document The document to schedule for insertion.
1221
     * @throws \InvalidArgumentException
1222
     */
1223 522
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1224
    {
1225 522
        $oid = spl_object_hash($document);
1226
1227 522
        if (isset($this->documentUpdates[$oid])) {
1228
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1229
        }
1230 522
        if (isset($this->documentDeletions[$oid])) {
1231
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1232
        }
1233 522
        if (isset($this->documentInsertions[$oid])) {
1234
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1235
        }
1236
1237 522
        $this->documentInsertions[$oid] = $document;
1238
1239 522
        if (isset($this->documentIdentifiers[$oid])) {
1240 519
            $this->addToIdentityMap($document);
1241 519
        }
1242 522
    }
1243
1244
    /**
1245
     * Schedules a document for upsert into the database and adds it to the
1246
     * identity map
1247
     *
1248
     * @param ClassMetadata $class
1249
     * @param object $document The document to schedule for upsert.
1250
     * @throws \InvalidArgumentException
1251
     */
1252 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1253
    {
1254 84
        $oid = spl_object_hash($document);
1255
1256 84
        if ($class->isEmbeddedDocument) {
1257
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1258
        }
1259 84
        if (isset($this->documentUpdates[$oid])) {
1260
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1261
        }
1262 84
        if (isset($this->documentDeletions[$oid])) {
1263
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1264
        }
1265 84
        if (isset($this->documentUpserts[$oid])) {
1266
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1267
        }
1268
1269 84
        $this->documentUpserts[$oid] = $document;
1270 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1271 84
        $this->addToIdentityMap($document);
1272 84
    }
1273
1274
    /**
1275
     * Checks whether a document is scheduled for insertion.
1276
     *
1277
     * @param object $document
1278
     * @return boolean
1279
     */
1280 101
    public function isScheduledForInsert($document)
1281
    {
1282 101
        return isset($this->documentInsertions[spl_object_hash($document)]);
1283
    }
1284
1285
    /**
1286
     * Checks whether a document is scheduled for upsert.
1287
     *
1288
     * @param object $document
1289
     * @return boolean
1290
     */
1291 5
    public function isScheduledForUpsert($document)
1292
    {
1293 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1294
    }
1295
1296
    /**
1297
     * Schedules a document for being updated.
1298
     *
1299
     * @param object $document The document to schedule for being updated.
1300
     * @throws \InvalidArgumentException
1301
     */
1302 229
    public function scheduleForUpdate($document)
1303
    {
1304 229
        $oid = spl_object_hash($document);
1305 229
        if ( ! isset($this->documentIdentifiers[$oid])) {
1306
            throw new \InvalidArgumentException("Document has no identity.");
1307
        }
1308
1309 229
        if (isset($this->documentDeletions[$oid])) {
1310
            throw new \InvalidArgumentException("Document is removed.");
1311
        }
1312
1313 229
        if ( ! isset($this->documentUpdates[$oid])
1314 229
            && ! isset($this->documentInsertions[$oid])
1315 229
            && ! isset($this->documentUpserts[$oid])) {
1316 225
            $this->documentUpdates[$oid] = $document;
1317 225
        }
1318 229
    }
1319
1320
    /**
1321
     * Checks whether a document is registered as dirty in the unit of work.
1322
     * Note: Is not very useful currently as dirty documents are only registered
1323
     * at commit time.
1324
     *
1325
     * @param object $document
1326
     * @return boolean
1327
     */
1328 13
    public function isScheduledForUpdate($document)
1329
    {
1330 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1331
    }
1332
1333 1
    public function isScheduledForDirtyCheck($document)
1334
    {
1335 1
        $class = $this->dm->getClassMetadata(get_class($document));
1336 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1337
    }
1338
1339
    /**
1340
     * INTERNAL:
1341
     * Schedules a document for deletion.
1342
     *
1343
     * @param object $document
1344
     */
1345 69
    public function scheduleForDelete($document)
1346
    {
1347 69
        $oid = spl_object_hash($document);
1348
1349 69
        if (isset($this->documentInsertions[$oid])) {
1350 2
            if ($this->isInIdentityMap($document)) {
1351 2
                $this->removeFromIdentityMap($document);
1352 2
            }
1353 2
            unset($this->documentInsertions[$oid]);
1354 2
            return; // document has not been persisted yet, so nothing more to do.
1355
        }
1356
1357 68
        if ( ! $this->isInIdentityMap($document)) {
1358 1
            return; // ignore
1359
        }
1360
1361 67
        $this->removeFromIdentityMap($document);
1362 67
        $this->documentStates[$oid] = self::STATE_REMOVED;
1363
1364 67
        if (isset($this->documentUpdates[$oid])) {
1365
            unset($this->documentUpdates[$oid]);
1366
        }
1367 67
        if ( ! isset($this->documentDeletions[$oid])) {
1368 67
            $this->documentDeletions[$oid] = $document;
1369 67
        }
1370 67
    }
1371
1372
    /**
1373
     * Checks whether a document is registered as removed/deleted with the unit
1374
     * of work.
1375
     *
1376
     * @param object $document
1377
     * @return boolean
1378
     */
1379 8
    public function isScheduledForDelete($document)
1380
    {
1381 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1382
    }
1383
1384
    /**
1385
     * Checks whether a document is scheduled for insertion, update or deletion.
1386
     *
1387
     * @param $document
1388
     * @return boolean
1389
     */
1390 229
    public function isDocumentScheduled($document)
1391
    {
1392 229
        $oid = spl_object_hash($document);
1393 229
        return isset($this->documentInsertions[$oid]) ||
1394 122
            isset($this->documentUpserts[$oid]) ||
1395 113
            isset($this->documentUpdates[$oid]) ||
1396 229
            isset($this->documentDeletions[$oid]);
1397
    }
1398
1399
    /**
1400
     * INTERNAL:
1401
     * Registers a document in the identity map.
1402
     *
1403
     * Note that documents in a hierarchy are registered with the class name of
1404
     * the root document. Identifiers are serialized before being used as array
1405
     * keys to allow differentiation of equal, but not identical, values.
1406
     *
1407
     * @ignore
1408
     * @param object $document  The document to register.
1409
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1410
     *                  the document in question is already managed.
1411
     */
1412 616
    public function addToIdentityMap($document)
1413
    {
1414 616
        $class = $this->dm->getClassMetadata(get_class($document));
1415 616
        $id = $this->getIdForIdentityMap($document);
1416
1417 616
        if (isset($this->identityMap[$class->name][$id])) {
1418 54
            return false;
1419
        }
1420
1421 616
        $this->identityMap[$class->name][$id] = $document;
1422
1423 616
        if ($document instanceof NotifyPropertyChanged &&
1424 616
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1425 3
            $document->addPropertyChangedListener($this);
1426 3
        }
1427
1428 616
        return true;
1429
    }
1430
1431
    /**
1432
     * Gets the state of a document with regard to the current unit of work.
1433
     *
1434
     * @param object   $document
1435
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1436
     *                         This parameter can be set to improve performance of document state detection
1437
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1438
     *                         is either known or does not matter for the caller of the method.
1439
     * @return int The document state.
1440
     */
1441 590
    public function getDocumentState($document, $assume = null)
1442
    {
1443 590
        $oid = spl_object_hash($document);
1444
1445 590
        if (isset($this->documentStates[$oid])) {
1446 359
            return $this->documentStates[$oid];
1447
        }
1448
1449 590
        $class = $this->dm->getClassMetadata(get_class($document));
1450
1451 590
        if ($class->isEmbeddedDocument) {
1452 179
            return self::STATE_NEW;
1453
        }
1454
1455 587
        if ($assume !== null) {
1456 584
            return $assume;
1457
        }
1458
1459
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1460
         * known. Note that you cannot remember the NEW or DETACHED state in
1461
         * _documentStates since the UoW does not hold references to such
1462
         * objects and the object hash can be reused. More generally, because
1463
         * the state may "change" between NEW/DETACHED without the UoW being
1464
         * aware of it.
1465
         */
1466 4
        $id = $class->getIdentifierObject($document);
1467
1468 4
        if ($id === null) {
1469 2
            return self::STATE_NEW;
1470
        }
1471
1472
        // Check for a version field, if available, to avoid a DB lookup.
1473 2
        if ($class->isVersioned) {
1474
            return ($class->getFieldValue($document, $class->versionField))
1475
                ? self::STATE_DETACHED
1476
                : self::STATE_NEW;
1477
        }
1478
1479
        // Last try before DB lookup: check the identity map.
1480 2
        if ($this->tryGetById($id, $class)) {
1481 1
            return self::STATE_DETACHED;
1482
        }
1483
1484
        // DB lookup
1485 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1486 1
            return self::STATE_DETACHED;
1487
        }
1488
1489 1
        return self::STATE_NEW;
1490
    }
1491
1492
    /**
1493
     * INTERNAL:
1494
     * Removes a document from the identity map. This effectively detaches the
1495
     * document from the persistence management of Doctrine.
1496
     *
1497
     * @ignore
1498
     * @param object $document
1499
     * @throws \InvalidArgumentException
1500
     * @return boolean
1501
     */
1502 78
    public function removeFromIdentityMap($document)
1503
    {
1504 78
        $oid = spl_object_hash($document);
1505
1506
        // Check if id is registered first
1507 78
        if ( ! isset($this->documentIdentifiers[$oid])) {
1508
            return false;
1509
        }
1510
1511 78
        $class = $this->dm->getClassMetadata(get_class($document));
1512 78
        $id = $this->getIdForIdentityMap($document);
1513
1514 78
        if (isset($this->identityMap[$class->name][$id])) {
1515 78
            unset($this->identityMap[$class->name][$id]);
1516 78
            $this->documentStates[$oid] = self::STATE_DETACHED;
1517 78
            return true;
1518
        }
1519
1520
        return false;
1521
    }
1522
1523
    /**
1524
     * INTERNAL:
1525
     * Gets a document in the identity map by its identifier hash.
1526
     *
1527
     * @ignore
1528
     * @param mixed         $id    Document identifier
1529
     * @param ClassMetadata $class Document class
1530
     * @return object
1531
     * @throws InvalidArgumentException if the class does not have an identifier
1532
     */
1533 32
    public function getById($id, ClassMetadata $class)
1534
    {
1535 32
        if ( ! $class->identifier) {
1536
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1537
        }
1538
1539 32
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1540
1541 32
        return $this->identityMap[$class->name][$serializedId];
1542
    }
1543
1544
    /**
1545
     * INTERNAL:
1546
     * Tries to get a document by its identifier hash. If no document is found
1547
     * for the given hash, FALSE is returned.
1548
     *
1549
     * @ignore
1550
     * @param mixed         $id    Document identifier
1551
     * @param ClassMetadata $class Document class
1552
     * @return mixed The found document or FALSE.
1553
     * @throws InvalidArgumentException if the class does not have an identifier
1554
     */
1555 294
    public function tryGetById($id, ClassMetadata $class)
1556
    {
1557 294
        if ( ! $class->identifier) {
1558
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1559
        }
1560
1561 294
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1562
1563 294
        return isset($this->identityMap[$class->name][$serializedId]) ?
1564 294
            $this->identityMap[$class->name][$serializedId] : false;
1565
    }
1566
1567
    /**
1568
     * Schedules a document for dirty-checking at commit-time.
1569
     *
1570
     * @param object $document The document to schedule for dirty-checking.
1571
     * @todo Rename: scheduleForSynchronization
1572
     */
1573 2
    public function scheduleForDirtyCheck($document)
1574
    {
1575 2
        $class = $this->dm->getClassMetadata(get_class($document));
1576 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1577 2
    }
1578
1579
    /**
1580
     * Checks whether a document is registered in the identity map.
1581
     *
1582
     * @param object $document
1583
     * @return boolean
1584
     */
1585 78
    public function isInIdentityMap($document)
1586
    {
1587 78
        $oid = spl_object_hash($document);
1588
1589 78
        if ( ! isset($this->documentIdentifiers[$oid])) {
1590 4
            return false;
1591
        }
1592
1593 77
        $class = $this->dm->getClassMetadata(get_class($document));
1594 77
        $id = $this->getIdForIdentityMap($document);
1595
1596 77
        return isset($this->identityMap[$class->name][$id]);
1597
    }
1598
1599
    /**
1600
     * @param object $document
1601
     * @return string
1602
     */
1603 616
    private function getIdForIdentityMap($document)
1604
    {
1605 616
        $class = $this->dm->getClassMetadata(get_class($document));
1606
1607 616
        if ( ! $class->identifier) {
1608 149
            $id = spl_object_hash($document);
1609 149
        } else {
1610 615
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1611 615
            $id = serialize($class->getDatabaseIdentifierValue($id));
1612
        }
1613
1614 616
        return $id;
1615
    }
1616
1617
    /**
1618
     * INTERNAL:
1619
     * Checks whether an identifier exists in the identity map.
1620
     *
1621
     * @ignore
1622
     * @param string $id
1623
     * @param string $rootClassName
1624
     * @return boolean
1625
     */
1626
    public function containsId($id, $rootClassName)
1627
    {
1628
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1629
    }
1630
1631
    /**
1632
     * Persists a document as part of the current unit of work.
1633
     *
1634
     * @param object $document The document to persist.
1635
     * @throws MongoDBException If trying to persist MappedSuperclass.
1636
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1637
     */
1638 585
    public function persist($document)
1639
    {
1640 585
        $class = $this->dm->getClassMetadata(get_class($document));
1641 585
        if ($class->isMappedSuperclass) {
1642 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1643
        }
1644 584
        $visited = array();
1645 584
        $this->doPersist($document, $visited);
1646 580
    }
1647
1648
    /**
1649
     * Saves a document as part of the current unit of work.
1650
     * This method is internally called during save() cascades as it tracks
1651
     * the already visited documents to prevent infinite recursions.
1652
     *
1653
     * NOTE: This method always considers documents that are not yet known to
1654
     * this UnitOfWork as NEW.
1655
     *
1656
     * @param object $document The document to persist.
1657
     * @param array $visited The already visited documents.
1658
     * @throws \InvalidArgumentException
1659
     * @throws MongoDBException
1660
     */
1661 584
    private function doPersist($document, array &$visited)
1662
    {
1663 584
        $oid = spl_object_hash($document);
1664 584
        if (isset($visited[$oid])) {
1665 24
            return; // Prevent infinite recursion
1666
        }
1667
1668 584
        $visited[$oid] = $document; // Mark visited
1669
1670 584
        $class = $this->dm->getClassMetadata(get_class($document));
1671
1672 584
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1673
        switch ($documentState) {
1674 584
            case self::STATE_MANAGED:
1675
                // Nothing to do, except if policy is "deferred explicit"
1676 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1677
                    $this->scheduleForDirtyCheck($document);
1678
                }
1679 44
                break;
1680 584
            case self::STATE_NEW:
1681 584
                $this->persistNew($class, $document);
1682 582
                break;
1683
1684 2
            case self::STATE_REMOVED:
1685
                // Document becomes managed again
1686 2
                unset($this->documentDeletions[$oid]);
1687
1688 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1689 2
                break;
1690
1691
            case self::STATE_DETACHED:
1692
                throw new \InvalidArgumentException(
1693
                    "Behavior of persist() for a detached document is not yet defined.");
1694
1695
            default:
1696
                throw MongoDBException::invalidDocumentState($documentState);
1697
        }
1698
1699 582
        $this->cascadePersist($document, $visited);
1700 580
    }
1701
1702
    /**
1703
     * Deletes a document as part of the current unit of work.
1704
     *
1705
     * @param object $document The document to remove.
1706
     */
1707 68
    public function remove($document)
1708
    {
1709 68
        $visited = array();
1710 68
        $this->doRemove($document, $visited);
1711 68
    }
1712
1713
    /**
1714
     * Deletes a document as part of the current unit of work.
1715
     *
1716
     * This method is internally called during delete() cascades as it tracks
1717
     * the already visited documents to prevent infinite recursions.
1718
     *
1719
     * @param object $document The document to delete.
1720
     * @param array $visited The map of the already visited documents.
1721
     * @throws MongoDBException
1722
     */
1723 68
    private function doRemove($document, array &$visited)
1724
    {
1725 68
        $oid = spl_object_hash($document);
1726 68
        if (isset($visited[$oid])) {
1727 1
            return; // Prevent infinite recursion
1728
        }
1729
1730 68
        $visited[$oid] = $document; // mark visited
1731
1732
        /* Cascade first, because scheduleForDelete() removes the entity from
1733
         * the identity map, which can cause problems when a lazy Proxy has to
1734
         * be initialized for the cascade operation.
1735
         */
1736 68
        $this->cascadeRemove($document, $visited);
1737
1738 68
        $class = $this->dm->getClassMetadata(get_class($document));
1739 68
        $documentState = $this->getDocumentState($document);
1740
        switch ($documentState) {
1741 68
            case self::STATE_NEW:
1742 68
            case self::STATE_REMOVED:
1743
                // nothing to do
1744 1
                break;
1745 68
            case self::STATE_MANAGED:
1746 68
                $this->lifecycleEventManager->preRemove($class, $document);
1747 68
                $this->scheduleForDelete($document);
1748 68
                break;
1749
            case self::STATE_DETACHED:
1750
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1751
            default:
1752
                throw MongoDBException::invalidDocumentState($documentState);
1753
        }
1754 68
    }
1755
1756
    /**
1757
     * Merges the state of the given detached document into this UnitOfWork.
1758
     *
1759
     * @param object $document
1760
     * @return object The managed copy of the document.
1761
     */
1762 13
    public function merge($document)
1763
    {
1764 13
        $visited = array();
1765
1766 13
        return $this->doMerge($document, $visited);
1767
    }
1768
1769
    /**
1770
     * Executes a merge operation on a document.
1771
     *
1772
     * @param object      $document
1773
     * @param array       $visited
1774
     * @param object|null $prevManagedCopy
1775
     * @param array|null  $assoc
1776
     *
1777
     * @return object The managed copy of the document.
1778
     *
1779
     * @throws InvalidArgumentException If the entity instance is NEW.
1780
     * @throws LockException If the document uses optimistic locking through a
1781
     *                       version attribute and the version check against the
1782
     *                       managed copy fails.
1783
     */
1784 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1785
    {
1786 13
        $oid = spl_object_hash($document);
1787
1788 13
        if (isset($visited[$oid])) {
1789 1
            return $visited[$oid]; // Prevent infinite recursion
1790
        }
1791
1792 13
        $visited[$oid] = $document; // mark visited
1793
1794 13
        $class = $this->dm->getClassMetadata(get_class($document));
1795
1796
        /* First we assume DETACHED, although it can still be NEW but we can
1797
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1798
         * an identity, we need to fetch it from the DB anyway in order to
1799
         * merge. MANAGED documents are ignored by the merge operation.
1800
         */
1801 13
        $managedCopy = $document;
1802
1803 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1804 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1805
                $document->__load();
1806
            }
1807
1808
            // Try to look the document up in the identity map.
1809 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1810
1811 13
            if ($id === null) {
1812
                // If there is no identifier, it is actually NEW.
1813 5
                $managedCopy = $class->newInstance();
1814 5
                $this->persistNew($class, $managedCopy);
1815 5
            } else {
1816 10
                $managedCopy = $this->tryGetById($id, $class);
1817
1818 10
                if ($managedCopy) {
1819
                    // We have the document in memory already, just make sure it is not removed.
1820 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1821
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1822
                    }
1823 5
                } else {
1824
                    // We need to fetch the managed copy in order to merge.
1825 7
                    $managedCopy = $this->dm->find($class->name, $id);
1826
                }
1827
1828 10
                if ($managedCopy === null) {
1829
                    // If the identifier is ASSIGNED, it is NEW
1830
                    $managedCopy = $class->newInstance();
1831
                    $class->setIdentifierValue($managedCopy, $id);
1832
                    $this->persistNew($class, $managedCopy);
1833
                } else {
1834 10
                    if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1835
                        $managedCopy->__load();
1836
                    }
1837
                }
1838
            }
1839
1840 13
            if ($class->isVersioned) {
1841
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1842
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1843
1844
                // Throw exception if versions don't match
1845
                if ($managedCopyVersion != $documentVersion) {
1846
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1847
                }
1848
            }
1849
1850
            // Merge state of $document into existing (managed) document
1851 13
            foreach ($class->reflClass->getProperties() as $prop) {
1852 13
                $name = $prop->name;
1853 13
                $prop->setAccessible(true);
1854 13
                if ( ! isset($class->associationMappings[$name])) {
1855 13
                    if ( ! $class->isIdentifier($name)) {
1856 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
1857 13
                    }
1858 13
                } else {
1859 13
                    $assoc2 = $class->associationMappings[$name];
1860
1861 13
                    if ($assoc2['type'] === 'one') {
1862 5
                        $other = $prop->getValue($document);
1863
1864 5
                        if ($other === null) {
1865 2
                            $prop->setValue($managedCopy, null);
1866 5
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
2089
    {
2090 12
        $class = $this->dm->getClassMetadata(get_class($document));
2091 12
        foreach ($class->fieldMappings as $mapping) {
2092 12
            if ( ! $mapping['isCascadeDetach']) {
2093 12
                continue;
2094
            }
2095 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2096 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2097 7
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2098
                    // Unwrap so that foreach() does not initialize
2099 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2100 6
                }
2101 7
                foreach ($relatedDocuments as $relatedDocument) {
2102 5
                    $this->doDetach($relatedDocument, $visited);
2103 7
                }
2104 7
            } elseif ($relatedDocuments !== null) {
2105 5
                $this->doDetach($relatedDocuments, $visited);
2106 5
            }
2107 12
        }
2108 12
    }
2109
    /**
2110
     * Cascades a merge operation to associated documents.
2111
     *
2112
     * @param object $document
2113
     * @param object $managedCopy
2114
     * @param array $visited
2115
     */
2116 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2117
    {
2118 13
        $class = $this->dm->getClassMetadata(get_class($document));
2119
2120 13
        $associationMappings = array_filter(
2121 13
            $class->associationMappings,
2122
            function ($assoc) { return $assoc['isCascadeMerge']; }
2123 13
        );
2124
2125 13
        foreach ($associationMappings as $assoc) {
2126 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2127
2128 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2129 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2130
                    // Collections are the same, so there is nothing to do
2131
                    continue;
2132
                }
2133
2134 8
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2135
                    // Unwrap so that foreach() does not initialize
2136 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2137 6
                }
2138
2139 8
                foreach ($relatedDocuments as $relatedDocument) {
2140 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2141 8
                }
2142 12
            } elseif ($relatedDocuments !== null) {
2143 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2144 3
            }
2145 13
        }
2146 13
    }
2147
2148
    /**
2149
     * Cascades the save operation to associated documents.
2150
     *
2151
     * @param object $document
2152
     * @param array $visited
2153
     */
2154 582
    private function cascadePersist($document, array &$visited)
2155
    {
2156 582
        $class = $this->dm->getClassMetadata(get_class($document));
2157
2158 582
        $associationMappings = array_filter(
2159 582
            $class->associationMappings,
2160
            function ($assoc) { return $assoc['isCascadePersist']; }
2161 582
        );
2162
2163 582
        foreach ($associationMappings as $fieldName => $mapping) {
2164 401
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2165
2166 401
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2167 344
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2168 17
                    if ($relatedDocuments->getOwner() !== $document) {
2169 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2170 2
                    }
2171
                    // Unwrap so that foreach() does not initialize
2172 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2173 17
                }
2174
2175 344
                $count = 0;
2176 344
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2177 190
                    if ( ! empty($mapping['embedded'])) {
2178 116
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2179 116
                        if ($knownParent && $knownParent !== $document) {
2180 4
                            $relatedDocument = clone $relatedDocument;
2181 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2182 4
                        }
2183 116
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2184 116
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2185 116
                    }
2186 190
                    $this->doPersist($relatedDocument, $visited);
2187 343
                }
2188 401
            } elseif ($relatedDocuments !== null) {
2189 120
                if ( ! empty($mapping['embedded'])) {
2190 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2191 66
                    if ($knownParent && $knownParent !== $document) {
2192 5
                        $relatedDocuments = clone $relatedDocuments;
2193 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2194 5
                    }
2195 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2196 66
                }
2197 120
                $this->doPersist($relatedDocuments, $visited);
2198 119
            }
2199 581
        }
2200 580
    }
2201
2202
    /**
2203
     * Cascades the delete operation to associated documents.
2204
     *
2205
     * @param object $document
2206
     * @param array $visited
2207
     */
2208 68 View Code Duplication
    private function cascadeRemove($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2209
    {
2210 68
        $class = $this->dm->getClassMetadata(get_class($document));
2211 68
        foreach ($class->fieldMappings as $mapping) {
2212 67
            if ( ! $mapping['isCascadeRemove']) {
2213 67
                continue;
2214
            }
2215 34
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2216 2
                $document->__load();
2217 2
            }
2218
2219 34
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2220 34
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2221
                // If its a PersistentCollection initialization is intended! No unwrap!
2222 25
                foreach ($relatedDocuments as $relatedDocument) {
2223 14
                    $this->doRemove($relatedDocument, $visited);
2224 25
                }
2225 34
            } elseif ($relatedDocuments !== null) {
2226 12
                $this->doRemove($relatedDocuments, $visited);
2227 12
            }
2228 68
        }
2229 68
    }
2230
2231
    /**
2232
     * Acquire a lock on the given document.
2233
     *
2234
     * @param object $document
2235
     * @param int $lockMode
2236
     * @param int $lockVersion
2237
     * @throws LockException
2238
     * @throws \InvalidArgumentException
2239
     */
2240 9
    public function lock($document, $lockMode, $lockVersion = null)
2241
    {
2242 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2243 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2244
        }
2245
2246 8
        $documentName = get_class($document);
2247 8
        $class = $this->dm->getClassMetadata($documentName);
2248
2249 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2250 3
            if ( ! $class->isVersioned) {
2251 1
                throw LockException::notVersioned($documentName);
2252
            }
2253
2254 2
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2255 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2256 2
                if ($documentVersion != $lockVersion) {
2257 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2258
                }
2259 1
            }
2260 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2261 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2262 5
        }
2263 6
    }
2264
2265
    /**
2266
     * Releases a lock on the given document.
2267
     *
2268
     * @param object $document
2269
     * @throws \InvalidArgumentException
2270
     */
2271 1
    public function unlock($document)
2272
    {
2273 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2274
            throw new \InvalidArgumentException("Document is not MANAGED.");
2275
        }
2276 1
        $documentName = get_class($document);
2277 1
        $this->getDocumentPersister($documentName)->unlock($document);
2278 1
    }
2279
2280
    /**
2281
     * Clears the UnitOfWork.
2282
     *
2283
     * @param string|null $documentName if given, only documents of this type will get detached.
2284
     */
2285 393
    public function clear($documentName = null)
2286
    {
2287 393
        if ($documentName === null) {
2288 387
            $this->identityMap =
2289 387
            $this->documentIdentifiers =
2290 387
            $this->originalDocumentData =
2291 387
            $this->documentChangeSets =
2292 387
            $this->documentStates =
2293 387
            $this->scheduledForDirtyCheck =
2294 387
            $this->documentInsertions =
2295 387
            $this->documentUpserts =
2296 387
            $this->documentUpdates =
2297 387
            $this->documentDeletions =
2298 387
            $this->collectionUpdates =
2299 387
            $this->collectionDeletions =
2300 387
            $this->parentAssociations =
2301 387
            $this->orphanRemovals = 
2302 387
            $this->hasScheduledCollections = array();
2303 387
        } else {
2304 6
            $visited = array();
2305 6
            foreach ($this->identityMap as $className => $documents) {
2306 6
                if ($className === $documentName) {
2307 3
                    foreach ($documents as $document) {
2308 3
                        $this->doDetach($document, $visited);
2309 3
                    }
2310 3
                }
2311 6
            }
2312
        }
2313
2314 393 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...
2315
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2316
        }
2317 393
    }
2318
2319
    /**
2320
     * INTERNAL:
2321
     * Schedules an embedded document for removal. The remove() operation will be
2322
     * invoked on that document at the beginning of the next commit of this
2323
     * UnitOfWork.
2324
     *
2325
     * @ignore
2326
     * @param object $document
2327
     */
2328 49
    public function scheduleOrphanRemoval($document)
2329
    {
2330 49
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2331 49
    }
2332
2333
    /**
2334
     * INTERNAL:
2335
     * Unschedules an embedded or referenced object for removal.
2336
     *
2337
     * @ignore
2338
     * @param object $document
2339
     */
2340 106
    public function unscheduleOrphanRemoval($document)
2341
    {
2342 106
        $oid = spl_object_hash($document);
2343 106
        if (isset($this->orphanRemovals[$oid])) {
2344 1
            unset($this->orphanRemovals[$oid]);
2345 1
        }
2346 106
    }
2347
2348
    /**
2349
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2350
     *  1) sets owner if it was cloned
2351
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2352
     *  3) NOP if state is OK
2353
     * Returned collection should be used from now on (only important with 2nd point)
2354
     *
2355
     * @param PersistentCollectionInterface $coll
2356
     * @param object $document
2357
     * @param ClassMetadata $class
2358
     * @param string $propName
2359
     * @return PersistentCollectionInterface
2360
     */
2361 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2362
    {
2363 8
        $owner = $coll->getOwner();
2364 8
        if ($owner === null) { // cloned
2365 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2366 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2367 2
            if ( ! $coll->isInitialized()) {
2368 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2369 1
            }
2370 2
            $newValue = clone $coll;
2371 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2372 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2373 2
            if ($this->isScheduledForUpdate($document)) {
2374
                // @todo following line should be superfluous once collections are stored in change sets
2375
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2376
            }
2377 2
            return $newValue;
2378
        }
2379 6
        return $coll;
2380
    }
2381
2382
    /**
2383
     * INTERNAL:
2384
     * Schedules a complete collection for removal when this UnitOfWork commits.
2385
     *
2386
     * @param PersistentCollectionInterface $coll
2387
     */
2388 42
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2389
    {
2390 42
        $oid = spl_object_hash($coll);
2391 42
        unset($this->collectionUpdates[$oid]);
2392 42
        if ( ! isset($this->collectionDeletions[$oid])) {
2393 42
            $this->collectionDeletions[$oid] = $coll;
2394 42
            $this->scheduleCollectionOwner($coll);
2395 42
        }
2396 42
    }
2397
2398
    /**
2399
     * Checks whether a PersistentCollection is scheduled for deletion.
2400
     *
2401
     * @param PersistentCollectionInterface $coll
2402
     * @return boolean
2403
     */
2404 207
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2405
    {
2406 207
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2407
    }
2408
    
2409
    /**
2410
     * INTERNAL:
2411
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2412
     * 
2413
     * @param \Doctrine\ODM\MongoDB\PersistentCollectionInterface $coll
2414
     */
2415 210 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...
2416
    {
2417 210
        $oid = spl_object_hash($coll);
2418 210
        if (isset($this->collectionDeletions[$oid])) {
2419 12
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2420 12
            unset($this->collectionDeletions[$oid]);
2421 12
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2422 12
        }
2423 210
    }
2424
2425
    /**
2426
     * INTERNAL:
2427
     * Schedules a collection for update when this UnitOfWork commits.
2428
     *
2429
     * @param PersistentCollectionInterface $coll
2430
     */
2431 226
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2432
    {
2433 226
        $mapping = $coll->getMapping();
2434 226
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2435
            /* There is no need to $unset collection if it will be $set later
2436
             * This is NOP if collection is not scheduled for deletion
2437
             */
2438 41
            $this->unscheduleCollectionDeletion($coll);
2439 41
        }
2440 226
        $oid = spl_object_hash($coll);
2441 226
        if ( ! isset($this->collectionUpdates[$oid])) {
2442 226
            $this->collectionUpdates[$oid] = $coll;
2443 226
            $this->scheduleCollectionOwner($coll);
2444 226
        }
2445 226
    }
2446
    
2447
    /**
2448
     * INTERNAL:
2449
     * Unschedules a collection from being updated when this UnitOfWork commits.
2450
     * 
2451
     * @param \Doctrine\ODM\MongoDB\PersistentCollectionInterface $coll
2452
     */
2453 210 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...
2454
    {
2455 210
        $oid = spl_object_hash($coll);
2456 210
        if (isset($this->collectionUpdates[$oid])) {
2457 200
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2458 200
            unset($this->collectionUpdates[$oid]);
2459 200
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2460 200
        }
2461 210
    }
2462
    
2463
    /**
2464
     * Checks whether a PersistentCollection is scheduled for update.
2465
     *
2466
     * @param PersistentCollectionInterface $coll
2467
     * @return boolean
2468
     */
2469 123
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2470
    {
2471 123
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2472
    }
2473
2474
    /**
2475
     * INTERNAL:
2476
     * Gets PersistentCollections that have been visited during computing change
2477
     * set of $document
2478
     *
2479
     * @param object $document
2480
     * @return PersistentCollectionInterface[]
2481
     */
2482 544
    public function getVisitedCollections($document)
2483
    {
2484 544
        $oid = spl_object_hash($document);
2485 544
        return isset($this->visitedCollections[$oid])
2486 544
                ? $this->visitedCollections[$oid]
2487 544
                : array();
2488
    }
2489
    
2490
    /**
2491
     * INTERNAL:
2492
     * Gets PersistentCollections that are scheduled to update and related to $document
2493
     * 
2494
     * @param object $document
2495
     * @return array
2496
     */
2497 544
    public function getScheduledCollections($document)
2498
    {
2499 544
        $oid = spl_object_hash($document);
2500 544
        return isset($this->hasScheduledCollections[$oid]) 
2501 544
                ? $this->hasScheduledCollections[$oid]
2502 544
                : array();
2503
    }
2504
    
2505
    /**
2506
     * Checks whether the document is related to a PersistentCollection
2507
     * scheduled for update or deletion.
2508
     *
2509
     * @param object $document
2510
     * @return boolean
2511
     */
2512 49
    public function hasScheduledCollections($document)
2513
    {
2514 49
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2515
    }
2516
    
2517
    /**
2518
     * Marks the PersistentCollection's top-level owner as having a relation to
2519
     * a collection scheduled for update or deletion.
2520
     *
2521
     * If the owner is not scheduled for any lifecycle action, it will be
2522
     * scheduled for update to ensure that versioning takes place if necessary.
2523
     *
2524
     * If the collection is nested within atomic collection, it is immediately
2525
     * unscheduled and atomic one is scheduled for update instead. This makes
2526
     * calculating update data way easier.
2527
     * 
2528
     * @param PersistentCollectionInterface $coll
2529
     */
2530 228
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2531
    {
2532 228
        $document = $this->getOwningDocument($coll->getOwner());
2533 228
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2534
2535 228
        if ($document !== $coll->getOwner()) {
2536 25
            $parent = $coll->getOwner();
2537 25
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2538 25
                list($mapping, $parent, ) = $parentAssoc;
2539 25
            }
2540 25
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2541 8
                $class = $this->dm->getClassMetadata(get_class($document));
2542 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2543 8
                $this->scheduleCollectionUpdate($atomicCollection);
2544 8
                $this->unscheduleCollectionDeletion($coll);
2545 8
                $this->unscheduleCollectionUpdate($coll);
2546 8
            }
2547 25
        }
2548
2549 228
        if ( ! $this->isDocumentScheduled($document)) {
2550 48
            $this->scheduleForUpdate($document);
2551 48
        }
2552 228
    }
2553
2554
    /**
2555
     * Get the top-most owning document of a given document
2556
     *
2557
     * If a top-level document is provided, that same document will be returned.
2558
     * For an embedded document, we will walk through parent associations until
2559
     * we find a top-level document.
2560
     *
2561
     * @param object $document
2562
     * @throws \UnexpectedValueException when a top-level document could not be found
2563
     * @return object
2564
     */
2565 230
    public function getOwningDocument($document)
2566
    {
2567 230
        $class = $this->dm->getClassMetadata(get_class($document));
2568 230
        while ($class->isEmbeddedDocument) {
2569 39
            $parentAssociation = $this->getParentAssociation($document);
2570
2571 39
            if ( ! $parentAssociation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentAssociation of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
2609 15
            $discriminatorValue = $data[$class->discriminatorField];
2610 189
        } elseif ($class->defaultDiscriminatorValue !== null) {
2611 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2612 1
        }
2613
2614 189
        if ($discriminatorValue !== null) {
2615 16
            return isset($class->discriminatorMap[$discriminatorValue])
2616 16
                ? $class->discriminatorMap[$discriminatorValue]
2617 16
                : $discriminatorValue;
2618
        }
2619
2620 173
        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 The document to be hydrated into in case of creation
2632
     * @return object The document instance.
2633
     * @internal Highly performance-sensitive method.
2634
     */
2635 388
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2636
    {
2637 388
        $class = $this->dm->getClassMetadata($className);
2638
2639
        // @TODO figure out how to remove this
2640 388
        $discriminatorValue = null;
2641 388 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 19
            $discriminatorValue = $data[$class->discriminatorField];
2643 388
        } elseif (isset($class->defaultDiscriminatorValue)) {
2644 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2645 2
        }
2646
2647 388
        if ($discriminatorValue !== null) {
2648 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2649 20
                ? $class->discriminatorMap[$discriminatorValue]
2650 20
                : $discriminatorValue;
2651
2652 20
            $class = $this->dm->getClassMetadata($className);
2653
2654 20
            unset($data[$class->discriminatorField]);
2655 20
        }
2656
2657 388
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2658 388
        $serializedId = serialize($id);
2659
2660 388
        if (isset($this->identityMap[$class->name][$serializedId])) {
2661 91
            $document = $this->identityMap[$class->name][$serializedId];
2662 91
            $oid = spl_object_hash($document);
2663 91
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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