Completed
Pull Request — master (#1331)
by Maciej
09:05
created

UnitOfWork::unscheduleCollectionDeletion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 9
Ratio 100 %

Code Coverage

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
565 12
            && ! isset($this->documentUpserts[$oid])
566 12
            && ! isset($this->documentDeletions[$oid])
567 12
            && isset($this->documentStates[$oid])
568 12
        ) {
569 8
            $this->computeChangeSet($class, $document);
570 8
        }
571 12
    }
572
573
    /**
574
     * Gets the changeset for a document.
575
     *
576
     * @param object $document
577
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
578
     */
579 545 View Code Duplication
    public function getDocumentChangeSet($document)
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...
580
    {
581 545
        $oid = spl_object_hash($document);
582 545
        if (isset($this->documentChangeSets[$oid])) {
583 545
            return $this->documentChangeSets[$oid];
584
        }
585 54
        return array();
586
    }
587
588
    /**
589
     * INTERNAL:
590
     * Gets the changeset for a document by reference.
591
     *
592
     * @param object $document
593
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
594
     */
595 219 View Code Duplication
    public function &getDocumentChangeSetByRef($document)
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...
596
    {
597 219
        $oid = spl_object_hash($document);
598 219
        if ( ! isset($this->documentChangeSets[$oid])) {
599 60
            $this->documentChangeSets[$oid] = array();
600 60
        }
601 219
        return $this->documentChangeSets[$oid];
602
    }
603
604
    /**
605
     * Get a documents actual data, flattening all the objects to arrays.
606
     *
607
     * @param object $document
608
     * @return array
609
     */
610 559
    public function getDocumentActualData($document)
611
    {
612 559
        $class = $this->dm->getClassMetadata(get_class($document));
613 559
        $actualData = array();
614 559
        foreach ($class->reflFields as $name => $refProp) {
615 559
            $mapping = $class->fieldMappings[$name];
616
            // skip not saved fields
617 559
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
618 49
                continue;
619
            }
620 559
            $value = $refProp->getValue($document);
621 559
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
622 5
                $value = new GridFSFile($value);
623 5
                $class->reflFields[$name]->setValue($document, $value);
624 5
                $actualData[$name] = $value;
625 559
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
626 559
                && $value !== null && ! ($value instanceof PersistentCollection)) {
627
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
628 366
                if ( ! $value instanceof Collection) {
629 119
                    $value = new ArrayCollection($value);
630 119
                }
631
632
                // Inject PersistentCollection
633 366
                $coll = new PersistentCollection($value, $this->dm, $this);
634 366
                $coll->setOwner($document, $mapping);
635 366
                $coll->setDirty( ! $value->isEmpty());
636 366
                $class->reflFields[$name]->setValue($document, $coll);
637 366
                $actualData[$name] = $coll;
638 366
            } else {
639 559
                $actualData[$name] = $value;
640
            }
641 559
        }
642 559
        return $actualData;
643
    }
644
645
    /**
646
     * Computes the changes that happened to a single document.
647
     *
648
     * Modifies/populates the following properties:
649
     *
650
     * {@link originalDocumentData}
651
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
652
     * then it was not fetched from the database and therefore we have no original
653
     * document data yet. All of the current document data is stored as the original document data.
654
     *
655
     * {@link documentChangeSets}
656
     * The changes detected on all properties of the document are stored there.
657
     * A change is a tuple array where the first entry is the old value and the second
658
     * entry is the new value of the property. Changesets are used by persisters
659
     * to INSERT/UPDATE the persistent document state.
660
     *
661
     * {@link documentUpdates}
662
     * If the document is already fully MANAGED (has been fetched from the database before)
663
     * and any changes to its properties are detected, then a reference to the document is stored
664
     * there to mark it for an update.
665
     *
666
     * @param ClassMetadata $class The class descriptor of the document.
667
     * @param object $document The document for which to compute the changes.
668
     */
669 559
    public function computeChangeSet(ClassMetadata $class, $document)
670
    {
671 559
        if ( ! $class->isInheritanceTypeNone()) {
672 172
            $class = $this->dm->getClassMetadata(get_class($document));
673 172
        }
674
675
        // Fire PreFlush lifecycle callbacks
676 559 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...
677 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
678 11
        }
679
680 559
        $this->computeOrRecomputeChangeSet($class, $document);
681 558
    }
682
683
    /**
684
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
685
     *
686
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
687
     * @param object $document
688
     * @param boolean $recompute
689
     */
690 559
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
691
    {
692 559
        $oid = spl_object_hash($document);
693 559
        $actualData = $this->getDocumentActualData($document);
694 559
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
695 559
        if ($isNewDocument) {
696
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
697
            // These result in an INSERT.
698 559
            $this->originalDocumentData[$oid] = $actualData;
699 559
            $changeSet = array();
700 559
            foreach ($actualData as $propName => $actualValue) {
701
                /* At this PersistentCollection shouldn't be here, probably it
702
                 * was cloned and its ownership must be fixed
703
                 */
704 559
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
705
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
706
                    $actualValue = $actualData[$propName];
707
                }
708
                // ignore inverse side of reference relationship
709 559 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...
710 172
                    continue;
711
                }
712 559
                $changeSet[$propName] = array(null, $actualValue);
713 559
            }
714 559
            $this->documentChangeSets[$oid] = $changeSet;
715 559
        } else {
716
            // Document is "fully" MANAGED: it was already fully persisted before
717
            // and we have a copy of the original data
718 279
            $originalData = $this->originalDocumentData[$oid];
719 279
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
720 279
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
721 2
                $changeSet = $this->documentChangeSets[$oid];
722 2
            } else {
723 279
                $changeSet = array();
724
            }
725
726 279
            foreach ($actualData as $propName => $actualValue) {
727
                // skip not saved fields
728 279
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
729
                    continue;
730
                }
731
732 279
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
733
734
                // skip if value has not changed
735 279
                if ($orgValue === $actualValue) {
736
                    // but consider dirty GridFSFile instances as changed
737 278
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
738 278
                        continue;
739
                    }
740 1
                }
741
742
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
743 180
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
744 11
                    if ($orgValue !== null) {
745 6
                        $this->scheduleOrphanRemoval($orgValue);
746 6
                    }
747
748 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
749 11
                    continue;
750
                }
751
752
                // if owning side of reference-one relationship
753 172
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
754 11
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
755 1
                        $this->scheduleOrphanRemoval($orgValue);
756 1
                    }
757
758 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
759 11
                    continue;
760
                }
761
762 164
                if ($isChangeTrackingNotify) {
763 2
                    continue;
764
                }
765
766
                // ignore inverse side of reference relationship
767 163 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...
768 2
                    continue;
769
                }
770
771
                // Persistent collection was exchanged with the "originally"
772
                // created one. This can only mean it was cloned and replaced
773
                // on another document.
774 163
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
775 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
776 6
                }
777
778
                // if embed-many or reference-many relationship
779 163
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
780 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
781
                    /* If original collection was exchanged with a non-empty value
782
                     * and $set will be issued, there is no need to $unset it first
783
                     */
784 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
785 7
                        continue;
786
                    }
787 19
                    if ($orgValue instanceof PersistentCollection) {
788 17
                        $this->scheduleCollectionDeletion($orgValue);
789 17
                    }
790 19
                    continue;
791
                }
792
793
                // skip equivalent date values
794 149
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
795 36
                    $dateType = Type::getType('date');
796 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
797 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
798
799 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
800 29
                        continue;
801
                    }
802 10
                }
803
804
                // regular field
805 133
                $changeSet[$propName] = array($orgValue, $actualValue);
806 279
            }
807 279
            if ($changeSet) {
808 166
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
809 166
                    ? $changeSet + $this->documentChangeSets[$oid]
810 17
                    : $changeSet;
811
812 166
                $this->originalDocumentData[$oid] = $actualData;
813 166
                $this->scheduleForUpdate($document);
814 166
            }
815
        }
816
817
        // Look for changes in associations of the document
818 559
        $associationMappings = array_filter(
819 559
            $class->associationMappings,
820
            function ($assoc) { return empty($assoc['notSaved']); }
821 559
        );
822
823 559
        foreach ($associationMappings as $mapping) {
824 430
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
825
826 430
            if ($value === null) {
827 290
                continue;
828
            }
829
830 421
            $this->computeAssociationChanges($document, $mapping, $value);
831
832 420
            if (isset($mapping['reference'])) {
833 316
                continue;
834
            }
835
836 328
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
837
838 328
            foreach ($values as $obj) {
839 170
                $oid2 = spl_object_hash($obj);
840
841 170
                if (isset($this->documentChangeSets[$oid2])) {
842 168
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
843
844 168
                    if ( ! $isNewDocument) {
845 72
                        $this->scheduleForUpdate($document);
846 72
                    }
847
848 168
                    break;
849
                }
850 328
            }
851 558
        }
852 558
    }
853
854
    /**
855
     * Computes all the changes that have been done to documents and collections
856
     * since the last commit and stores these changes in the _documentChangeSet map
857
     * temporarily for access by the persisters, until the UoW commit is finished.
858
     */
859 557
    public function computeChangeSets()
860
    {
861 557
        $this->computeScheduleInsertsChangeSets();
862 556
        $this->computeScheduleUpsertsChangeSets();
863
864
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
865 556
        foreach ($this->identityMap as $className => $documents) {
866 556
            $class = $this->dm->getClassMetadata($className);
867 556
            if ($class->isEmbeddedDocument) {
868
                /* we do not want to compute changes to embedded documents up front
869
                 * in case embedded document was replaced and its changeset
870
                 * would corrupt data. Embedded documents' change set will
871
                 * be calculated by reachability from owning document.
872
                 */
873 159
                continue;
874
            }
875
876
            // If change tracking is explicit or happens through notification, then only compute
877
            // changes on document of that type that are explicitly marked for synchronization.
878 556
            switch (true) {
879 556
                case ($class->isChangeTrackingDeferredImplicit()):
880 555
                    $documentsToProcess = $documents;
881 555
                    break;
882
883 3
                case (isset($this->scheduledForDirtyCheck[$className])):
884 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
885 2
                    break;
886
887 3
                default:
888 3
                    $documentsToProcess = array();
889
890 3
            }
891
892 556
            foreach ($documentsToProcess as $document) {
893
                // Ignore uninitialized proxy objects
894 552
                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...
895 10
                    continue;
896
                }
897
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
898 552
                $oid = spl_object_hash($document);
899 552 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...
900 552
                    && ! isset($this->documentUpserts[$oid])
901 552
                    && ! isset($this->documentDeletions[$oid])
902 552
                    && isset($this->documentStates[$oid])
903 552
                ) {
904 264
                    $this->computeChangeSet($class, $document);
905 264
                }
906 556
            }
907 556
        }
908 556
    }
909
910
    /**
911
     * Computes the changes of an association.
912
     *
913
     * @param object $parentDocument
914
     * @param array $assoc
915
     * @param mixed $value The value of the association.
916
     * @throws \InvalidArgumentException
917
     */
918 421
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
919
    {
920 421
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
921 421
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
922 421
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
923
924 421
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
925 8
            return;
926
        }
927
928 420
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
929 226
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
930 222
                $this->scheduleCollectionUpdate($value);
931 222
            }
932 226
            $topmostOwner = $this->getOwningDocument($value->getOwner());
933 226
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
934 226
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
935 132
                $value->initialize();
936 132
                foreach ($value->getDeletedDocuments() as $orphan) {
937 21
                    $this->scheduleOrphanRemoval($orphan);
938 132
                }
939 132
            }
940 226
        }
941
942
        // Look through the documents, and in any of their associations,
943
        // for transient (new) documents, recursively. ("Persistence by reachability")
944
        // Unwrap. Uninitialized collections will simply be empty.
945 420
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
946
947 420
        $count = 0;
948 420
        foreach ($unwrappedValue as $key => $entry) {
949 325
            if ( ! is_object($entry)) {
950 1
                throw new \InvalidArgumentException(
951 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
952 1
                );
953
            }
954
955 324
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
956
957 324
            $state = $this->getDocumentState($entry, self::STATE_NEW);
958
959
            // Handle "set" strategy for multi-level hierarchy
960 324
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
961 324
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
962
963 324
            $count++;
964
965
            switch ($state) {
966 324
                case self::STATE_NEW:
967 57
                    if ( ! $assoc['isCascadePersist']) {
968
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
969
                            . " configured to cascade persist operations: " . $this->objToStr($entry) . "."
970
                            . " Explicitly persist the new document or configure cascading persist operations"
971
                            . " on the relationship.");
972
                    }
973
974 57
                    $this->persistNew($targetClass, $entry);
975 57
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
976 57
                    $this->computeChangeSet($targetClass, $entry);
977 57
                    break;
978
979 319
                case self::STATE_MANAGED:
980 319
                    if ($targetClass->isEmbeddedDocument) {
981 161
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
982 161
                        if ($knownParent && $knownParent !== $parentDocument) {
983 6
                            $entry = clone $entry;
984 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
985 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
986 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
987 3
                            } else {
988
                                // must use unwrapped value to not trigger orphan removal
989 6
                                $unwrappedValue[$key] = $entry;
990
                            }
991 6
                            $this->persistNew($targetClass, $entry);
992 6
                        }
993 161
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
994 161
                        $this->computeChangeSet($targetClass, $entry);
995 161
                    }
996 319
                    break;
997
998 1
                case self::STATE_REMOVED:
999
                    // Consume the $value as array (it's either an array or an ArrayAccess)
1000
                    // and remove the element from Collection.
1001 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
1002
                        unset($value[$key]);
1003
                    }
1004 1
                    break;
1005
1006
                case self::STATE_DETACHED:
1007
                    // Can actually not happen right now as we assume STATE_NEW,
1008
                    // so the exception will be raised from the DBAL layer (constraint violation).
1009
                    throw new \InvalidArgumentException("A detached document was found through a "
1010
                        . "relationship during cascading a persist operation.");
1011
1012
                default:
1013
                    // MANAGED associated documents are already taken into account
1014
                    // during changeset calculation anyway, since they are in the identity map.
1015
1016
            }
1017 419
        }
1018 419
    }
1019
1020
    /**
1021
     * INTERNAL:
1022
     * Computes the changeset of an individual document, independently of the
1023
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1024
     *
1025
     * The passed document must be a managed document. If the document already has a change set
1026
     * because this method is invoked during a commit cycle then the change sets are added.
1027
     * whereby changes detected in this method prevail.
1028
     *
1029
     * @ignore
1030
     * @param ClassMetadata $class The class descriptor of the document.
1031
     * @param object $document The document for which to (re)calculate the change set.
1032
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1033
     */
1034 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1035
    {
1036
        // Ignore uninitialized proxy objects
1037 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...
1038 1
            return;
1039
        }
1040
1041 19
        $oid = spl_object_hash($document);
1042
1043 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1044
            throw new \InvalidArgumentException('Document must be managed.');
1045
        }
1046
1047 19
        if ( ! $class->isInheritanceTypeNone()) {
1048 2
            $class = $this->dm->getClassMetadata(get_class($document));
1049 2
        }
1050
1051 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1052 19
    }
1053
1054
    /**
1055
     * @param ClassMetadata $class
1056
     * @param object $document
1057
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1058
     */
1059 577
    private function persistNew(ClassMetadata $class, $document)
1060
    {
1061 577
        $this->lifecycleEventManager->prePersist($class, $document);
1062 577
        $oid = spl_object_hash($document);
1063 577
        $upsert = false;
1064 577
        if ($class->identifier) {
1065 577
            $idValue = $class->getIdentifierValue($document);
1066 577
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1067
1068 577
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1069 3
                throw new \InvalidArgumentException(sprintf(
1070 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1071 3
                    get_class($document)
1072 3
                ));
1073
            }
1074
1075 576
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1076 1
                throw new \InvalidArgumentException(sprintf(
1077 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1078 1
                    get_class($document)
1079 1
                ));
1080
            }
1081
1082 575
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1083 504
                $idValue = $class->idGenerator->generate($this->dm, $document);
1084 504
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1085 504
                $class->setIdentifierValue($document, $idValue);
1086 504
            }
1087
1088 575
            $this->documentIdentifiers[$oid] = $idValue;
1089 575
        } else {
1090
            // this is for embedded documents without identifiers
1091 143
            $this->documentIdentifiers[$oid] = $oid;
1092
        }
1093
1094 575
        $this->documentStates[$oid] = self::STATE_MANAGED;
1095
1096 575
        if ($upsert) {
1097 81
            $this->scheduleForUpsert($class, $document);
1098 81
        } else {
1099 509
            $this->scheduleForInsert($class, $document);
1100
        }
1101 575
    }
1102
1103
    /**
1104
     * Executes all document insertions for documents of the specified type.
1105
     *
1106
     * @param ClassMetadata $class
1107
     * @param array $documents Array of documents to insert
1108
     * @param array $options Array of options to be used with batchInsert()
1109
     */
1110 488 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...
1111
    {
1112 488
        $persister = $this->getDocumentPersister($class->name);
1113
1114 488
        foreach ($documents as $oid => $document) {
1115 488
            $persister->addInsert($document);
1116 488
            unset($this->documentInsertions[$oid]);
1117 488
        }
1118
1119 488
        $persister->executeInserts($options);
1120
1121 487
        foreach ($documents as $document) {
1122 487
            $this->lifecycleEventManager->postPersist($class, $document);
1123 487
        }
1124 487
    }
1125
1126
    /**
1127
     * Executes all document upserts for documents of the specified type.
1128
     *
1129
     * @param ClassMetadata $class
1130
     * @param array $documents Array of documents to upsert
1131
     * @param array $options Array of options to be used with batchInsert()
1132
     */
1133 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...
1134
    {
1135 78
        $persister = $this->getDocumentPersister($class->name);
1136
1137
1138 78
        foreach ($documents as $oid => $document) {
1139 78
            $persister->addUpsert($document);
1140 78
            unset($this->documentUpserts[$oid]);
1141 78
        }
1142
1143 78
        $persister->executeUpserts($options);
1144
1145 78
        foreach ($documents as $document) {
1146 78
            $this->lifecycleEventManager->postPersist($class, $document);
1147 78
        }
1148 78
    }
1149
1150
    /**
1151
     * Executes all document updates for documents of the specified type.
1152
     *
1153
     * @param Mapping\ClassMetadata $class
1154
     * @param array $documents Array of documents to update
1155
     * @param array $options Array of options to be used with update()
1156
     */
1157 219
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1158
    {
1159 219
        $className = $class->name;
1160 219
        $persister = $this->getDocumentPersister($className);
1161
1162 219
        foreach ($documents as $oid => $document) {
1163 219
            $this->lifecycleEventManager->preUpdate($class, $document);
1164
1165 219
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1166 217
                $persister->update($document, $options);
1167 213
            }
1168
1169 215
            unset($this->documentUpdates[$oid]);
1170
1171 215
            $this->lifecycleEventManager->postUpdate($class, $document);
1172 215
        }
1173 214
    }
1174
1175
    /**
1176
     * Executes all document deletions for documents of the specified type.
1177
     *
1178
     * @param ClassMetadata $class
1179
     * @param array $documents Array of documents to delete
1180
     * @param array $options Array of options to be used with remove()
1181
     */
1182 63
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1183
    {
1184 63
        $persister = $this->getDocumentPersister($class->name);
1185
1186 63
        foreach ($documents as $oid => $document) {
1187 63
            if ( ! $class->isEmbeddedDocument) {
1188 28
                $persister->delete($document, $options);
1189 26
            }
1190
            unset(
1191 61
                $this->documentDeletions[$oid],
1192 61
                $this->documentIdentifiers[$oid],
1193 61
                $this->originalDocumentData[$oid]
1194
            );
1195
1196
            // Clear snapshot information for any referenced PersistentCollection
1197
            // http://www.doctrine-project.org/jira/browse/MODM-95
1198 61
            foreach ($class->associationMappings as $fieldMapping) {
1199 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1200 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1201 26
                    if ($value instanceof PersistentCollection) {
1202 22
                        $value->clearSnapshot();
1203 22
                    }
1204 26
                }
1205 61
            }
1206
1207
            // Document with this $oid after deletion treated as NEW, even if the $oid
1208
            // is obtained by a new document because the old one went out of scope.
1209 61
            $this->documentStates[$oid] = self::STATE_NEW;
1210
1211 61
            $this->lifecycleEventManager->postRemove($class, $document);
1212 61
        }
1213 61
    }
1214
1215
    /**
1216
     * Schedules a document for insertion into the database.
1217
     * If the document already has an identifier, it will be added to the
1218
     * identity map.
1219
     *
1220
     * @param ClassMetadata $class
1221
     * @param object $document The document to schedule for insertion.
1222
     * @throws \InvalidArgumentException
1223
     */
1224 512
    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...
1225
    {
1226 512
        $oid = spl_object_hash($document);
1227
1228 512
        if (isset($this->documentUpdates[$oid])) {
1229
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1230
        }
1231 512
        if (isset($this->documentDeletions[$oid])) {
1232
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1233
        }
1234 512
        if (isset($this->documentInsertions[$oid])) {
1235
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1236
        }
1237
1238 512
        $this->documentInsertions[$oid] = $document;
1239
1240 512
        if (isset($this->documentIdentifiers[$oid])) {
1241 509
            $this->addToIdentityMap($document);
1242 509
        }
1243 512
    }
1244
1245
    /**
1246
     * Schedules a document for upsert into the database and adds it to the
1247
     * identity map
1248
     *
1249
     * @param ClassMetadata $class
1250
     * @param object $document The document to schedule for upsert.
1251
     * @throws \InvalidArgumentException
1252
     */
1253 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1254
    {
1255 84
        $oid = spl_object_hash($document);
1256
1257 84
        if ($class->isEmbeddedDocument) {
1258
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1259
        }
1260 84
        if (isset($this->documentUpdates[$oid])) {
1261
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1262
        }
1263 84
        if (isset($this->documentDeletions[$oid])) {
1264
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1265
        }
1266 84
        if (isset($this->documentUpserts[$oid])) {
1267
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1268
        }
1269
1270 84
        $this->documentUpserts[$oid] = $document;
1271 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1272 84
        $this->addToIdentityMap($document);
1273 84
    }
1274
1275
    /**
1276
     * Checks whether a document is scheduled for insertion.
1277
     *
1278
     * @param object $document
1279
     * @return boolean
1280
     */
1281 100
    public function isScheduledForInsert($document)
1282
    {
1283 100
        return isset($this->documentInsertions[spl_object_hash($document)]);
1284
    }
1285
1286
    /**
1287
     * Checks whether a document is scheduled for upsert.
1288
     *
1289
     * @param object $document
1290
     * @return boolean
1291
     */
1292 5
    public function isScheduledForUpsert($document)
1293
    {
1294 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1295
    }
1296
1297
    /**
1298
     * Schedules a document for being updated.
1299
     *
1300
     * @param object $document The document to schedule for being updated.
1301
     * @throws \InvalidArgumentException
1302
     */
1303 228
    public function scheduleForUpdate($document)
1304
    {
1305 228
        $oid = spl_object_hash($document);
1306 228
        if ( ! isset($this->documentIdentifiers[$oid])) {
1307
            throw new \InvalidArgumentException("Document has no identity.");
1308
        }
1309
1310 228
        if (isset($this->documentDeletions[$oid])) {
1311
            throw new \InvalidArgumentException("Document is removed.");
1312
        }
1313
1314 228
        if ( ! isset($this->documentUpdates[$oid])
1315 228
            && ! isset($this->documentInsertions[$oid])
1316 228
            && ! isset($this->documentUpserts[$oid])) {
1317 224
            $this->documentUpdates[$oid] = $document;
1318 224
        }
1319 228
    }
1320
1321
    /**
1322
     * Checks whether a document is registered as dirty in the unit of work.
1323
     * Note: Is not very useful currently as dirty documents are only registered
1324
     * at commit time.
1325
     *
1326
     * @param object $document
1327
     * @return boolean
1328
     */
1329 13
    public function isScheduledForUpdate($document)
1330
    {
1331 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1332
    }
1333
1334 1
    public function isScheduledForDirtyCheck($document)
1335
    {
1336 1
        $class = $this->dm->getClassMetadata(get_class($document));
1337 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1338
    }
1339
1340
    /**
1341
     * INTERNAL:
1342
     * Schedules a document for deletion.
1343
     *
1344
     * @param object $document
1345
     */
1346 68
    public function scheduleForDelete($document)
1347
    {
1348 68
        $oid = spl_object_hash($document);
1349
1350 68
        if (isset($this->documentInsertions[$oid])) {
1351 2
            if ($this->isInIdentityMap($document)) {
1352 2
                $this->removeFromIdentityMap($document);
1353 2
            }
1354 2
            unset($this->documentInsertions[$oid]);
1355 2
            return; // document has not been persisted yet, so nothing more to do.
1356
        }
1357
1358 67
        if ( ! $this->isInIdentityMap($document)) {
1359 1
            return; // ignore
1360
        }
1361
1362 66
        $this->removeFromIdentityMap($document);
1363 66
        $this->documentStates[$oid] = self::STATE_REMOVED;
1364
1365 66
        if (isset($this->documentUpdates[$oid])) {
1366
            unset($this->documentUpdates[$oid]);
1367
        }
1368 66
        if ( ! isset($this->documentDeletions[$oid])) {
1369 66
            $this->documentDeletions[$oid] = $document;
1370 66
        }
1371 66
    }
1372
1373
    /**
1374
     * Checks whether a document is registered as removed/deleted with the unit
1375
     * of work.
1376
     *
1377
     * @param object $document
1378
     * @return boolean
1379
     */
1380 8
    public function isScheduledForDelete($document)
1381
    {
1382 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1383
    }
1384
1385
    /**
1386
     * Checks whether a document is scheduled for insertion, update or deletion.
1387
     *
1388
     * @param $document
1389
     * @return boolean
1390
     */
1391 225
    public function isDocumentScheduled($document)
1392
    {
1393 225
        $oid = spl_object_hash($document);
1394 225
        return isset($this->documentInsertions[$oid]) ||
1395 121
            isset($this->documentUpserts[$oid]) ||
1396 112
            isset($this->documentUpdates[$oid]) ||
1397 225
            isset($this->documentDeletions[$oid]);
1398
    }
1399
1400
    /**
1401
     * INTERNAL:
1402
     * Registers a document in the identity map.
1403
     *
1404
     * Note that documents in a hierarchy are registered with the class name of
1405
     * the root document. Identifiers are serialized before being used as array
1406
     * keys to allow differentiation of equal, but not identical, values.
1407
     *
1408
     * @ignore
1409
     * @param object $document  The document to register.
1410
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1411
     *                  the document in question is already managed.
1412
     */
1413 606
    public function addToIdentityMap($document)
1414
    {
1415 606
        $class = $this->dm->getClassMetadata(get_class($document));
1416 606
        $id = $this->getIdForIdentityMap($document);
1417
1418 606
        if (isset($this->identityMap[$class->name][$id])) {
1419 53
            return false;
1420
        }
1421
1422 606
        $this->identityMap[$class->name][$id] = $document;
1423
1424 606
        if ($document instanceof NotifyPropertyChanged &&
1425 606
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1426 3
            $document->addPropertyChangedListener($this);
1427 3
        }
1428
1429 606
        return true;
1430
    }
1431
1432
    /**
1433
     * Gets the state of a document with regard to the current unit of work.
1434
     *
1435
     * @param object   $document
1436
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1437
     *                         This parameter can be set to improve performance of document state detection
1438
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1439
     *                         is either known or does not matter for the caller of the method.
1440
     * @return int The document state.
1441
     */
1442 580
    public function getDocumentState($document, $assume = null)
1443
    {
1444 580
        $oid = spl_object_hash($document);
1445
1446 580
        if (isset($this->documentStates[$oid])) {
1447 355
            return $this->documentStates[$oid];
1448
        }
1449
1450 580
        $class = $this->dm->getClassMetadata(get_class($document));
1451
1452 580
        if ($class->isEmbeddedDocument) {
1453 176
            return self::STATE_NEW;
1454
        }
1455
1456 577
        if ($assume !== null) {
1457 574
            return $assume;
1458
        }
1459
1460
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1461
         * known. Note that you cannot remember the NEW or DETACHED state in
1462
         * _documentStates since the UoW does not hold references to such
1463
         * objects and the object hash can be reused. More generally, because
1464
         * the state may "change" between NEW/DETACHED without the UoW being
1465
         * aware of it.
1466
         */
1467 4
        $id = $class->getIdentifierObject($document);
1468
1469 4
        if ($id === null) {
1470 2
            return self::STATE_NEW;
1471
        }
1472
1473
        // Check for a version field, if available, to avoid a DB lookup.
1474 2
        if ($class->isVersioned) {
1475
            return ($class->getFieldValue($document, $class->versionField))
1476
                ? self::STATE_DETACHED
1477
                : self::STATE_NEW;
1478
        }
1479
1480
        // Last try before DB lookup: check the identity map.
1481 2
        if ($this->tryGetById($id, $class)) {
1482 1
            return self::STATE_DETACHED;
1483
        }
1484
1485
        // DB lookup
1486 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1487 1
            return self::STATE_DETACHED;
1488
        }
1489
1490 1
        return self::STATE_NEW;
1491
    }
1492
1493
    /**
1494
     * INTERNAL:
1495
     * Removes a document from the identity map. This effectively detaches the
1496
     * document from the persistence management of Doctrine.
1497
     *
1498
     * @ignore
1499
     * @param object $document
1500
     * @throws \InvalidArgumentException
1501
     * @return boolean
1502
     */
1503 77
    public function removeFromIdentityMap($document)
1504
    {
1505 77
        $oid = spl_object_hash($document);
1506
1507
        // Check if id is registered first
1508 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1509
            return false;
1510
        }
1511
1512 77
        $class = $this->dm->getClassMetadata(get_class($document));
1513 77
        $id = $this->getIdForIdentityMap($document);
1514
1515 77
        if (isset($this->identityMap[$class->name][$id])) {
1516 77
            unset($this->identityMap[$class->name][$id]);
1517 77
            $this->documentStates[$oid] = self::STATE_DETACHED;
1518 77
            return true;
1519
        }
1520
1521
        return false;
1522
    }
1523
1524
    /**
1525
     * INTERNAL:
1526
     * Gets a document in the identity map by its identifier hash.
1527
     *
1528
     * @ignore
1529
     * @param mixed         $id    Document identifier
1530
     * @param ClassMetadata $class Document class
1531
     * @return object
1532
     * @throws InvalidArgumentException if the class does not have an identifier
1533
     */
1534 31
    public function getById($id, ClassMetadata $class)
1535
    {
1536 31
        if ( ! $class->identifier) {
1537
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1538
        }
1539
1540 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1541
1542 31
        return $this->identityMap[$class->name][$serializedId];
1543
    }
1544
1545
    /**
1546
     * INTERNAL:
1547
     * Tries to get a document by its identifier hash. If no document is found
1548
     * for the given hash, FALSE is returned.
1549
     *
1550
     * @ignore
1551
     * @param mixed         $id    Document identifier
1552
     * @param ClassMetadata $class Document class
1553
     * @return mixed The found document or FALSE.
1554
     * @throws InvalidArgumentException if the class does not have an identifier
1555
     */
1556 291
    public function tryGetById($id, ClassMetadata $class)
1557
    {
1558 291
        if ( ! $class->identifier) {
1559
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1560
        }
1561
1562 291
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1563
1564 291
        return isset($this->identityMap[$class->name][$serializedId]) ?
1565 291
            $this->identityMap[$class->name][$serializedId] : false;
1566
    }
1567
1568
    /**
1569
     * Schedules a document for dirty-checking at commit-time.
1570
     *
1571
     * @param object $document The document to schedule for dirty-checking.
1572
     * @todo Rename: scheduleForSynchronization
1573
     */
1574 2
    public function scheduleForDirtyCheck($document)
1575
    {
1576 2
        $class = $this->dm->getClassMetadata(get_class($document));
1577 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1578 2
    }
1579
1580
    /**
1581
     * Checks whether a document is registered in the identity map.
1582
     *
1583
     * @param object $document
1584
     * @return boolean
1585
     */
1586 77
    public function isInIdentityMap($document)
1587
    {
1588 77
        $oid = spl_object_hash($document);
1589
1590 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1591 4
            return false;
1592
        }
1593
1594 76
        $class = $this->dm->getClassMetadata(get_class($document));
1595 76
        $id = $this->getIdForIdentityMap($document);
1596
1597 76
        return isset($this->identityMap[$class->name][$id]);
1598
    }
1599
1600
    /**
1601
     * @param object $document
1602
     * @return string
1603
     */
1604 606
    private function getIdForIdentityMap($document)
1605
    {
1606 606
        $class = $this->dm->getClassMetadata(get_class($document));
1607
1608 606
        if ( ! $class->identifier) {
1609 146
            $id = spl_object_hash($document);
1610 146
        } else {
1611 605
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1612 605
            $id = serialize($class->getDatabaseIdentifierValue($id));
1613
        }
1614
1615 606
        return $id;
1616
    }
1617
1618
    /**
1619
     * INTERNAL:
1620
     * Checks whether an identifier exists in the identity map.
1621
     *
1622
     * @ignore
1623
     * @param string $id
1624
     * @param string $rootClassName
1625
     * @return boolean
1626
     */
1627
    public function containsId($id, $rootClassName)
1628
    {
1629
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1630
    }
1631
1632
    /**
1633
     * Persists a document as part of the current unit of work.
1634
     *
1635
     * @param object $document The document to persist.
1636
     * @throws MongoDBException If trying to persist MappedSuperclass.
1637
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1638
     */
1639 575
    public function persist($document)
1640
    {
1641 575
        $class = $this->dm->getClassMetadata(get_class($document));
1642 575
        if ($class->isMappedSuperclass) {
1643 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1644
        }
1645 574
        $visited = array();
1646 574
        $this->doPersist($document, $visited);
1647 570
    }
1648
1649
    /**
1650
     * Saves a document as part of the current unit of work.
1651
     * This method is internally called during save() cascades as it tracks
1652
     * the already visited documents to prevent infinite recursions.
1653
     *
1654
     * NOTE: This method always considers documents that are not yet known to
1655
     * this UnitOfWork as NEW.
1656
     *
1657
     * @param object $document The document to persist.
1658
     * @param array $visited The already visited documents.
1659
     * @throws \InvalidArgumentException
1660
     * @throws MongoDBException
1661
     */
1662 574
    private function doPersist($document, array &$visited)
1663
    {
1664 574
        $oid = spl_object_hash($document);
1665 574
        if (isset($visited[$oid])) {
1666 24
            return; // Prevent infinite recursion
1667
        }
1668
1669 574
        $visited[$oid] = $document; // Mark visited
1670
1671 574
        $class = $this->dm->getClassMetadata(get_class($document));
1672
1673 574
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1674
        switch ($documentState) {
1675 574
            case self::STATE_MANAGED:
1676
                // Nothing to do, except if policy is "deferred explicit"
1677 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1678
                    $this->scheduleForDirtyCheck($document);
1679
                }
1680 44
                break;
1681 574
            case self::STATE_NEW:
1682 574
                $this->persistNew($class, $document);
1683 572
                break;
1684
1685 2
            case self::STATE_REMOVED:
1686
                // Document becomes managed again
1687 2
                unset($this->documentDeletions[$oid]);
1688
1689 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1690 2
                break;
1691
1692
            case self::STATE_DETACHED:
1693
                throw new \InvalidArgumentException(
1694
                    "Behavior of persist() for a detached document is not yet defined.");
1695
1696
            default:
1697
                throw MongoDBException::invalidDocumentState($documentState);
1698
        }
1699
1700 572
        $this->cascadePersist($document, $visited);
1701 570
    }
1702
1703
    /**
1704
     * Deletes a document as part of the current unit of work.
1705
     *
1706
     * @param object $document The document to remove.
1707
     */
1708 67
    public function remove($document)
1709
    {
1710 67
        $visited = array();
1711 67
        $this->doRemove($document, $visited);
1712 67
    }
1713
1714
    /**
1715
     * Deletes a document as part of the current unit of work.
1716
     *
1717
     * This method is internally called during delete() cascades as it tracks
1718
     * the already visited documents to prevent infinite recursions.
1719
     *
1720
     * @param object $document The document to delete.
1721
     * @param array $visited The map of the already visited documents.
1722
     * @throws MongoDBException
1723
     */
1724 67
    private function doRemove($document, array &$visited)
1725
    {
1726 67
        $oid = spl_object_hash($document);
1727 67
        if (isset($visited[$oid])) {
1728 1
            return; // Prevent infinite recursion
1729
        }
1730
1731 67
        $visited[$oid] = $document; // mark visited
1732
1733
        /* Cascade first, because scheduleForDelete() removes the entity from
1734
         * the identity map, which can cause problems when a lazy Proxy has to
1735
         * be initialized for the cascade operation.
1736
         */
1737 67
        $this->cascadeRemove($document, $visited);
1738
1739 67
        $class = $this->dm->getClassMetadata(get_class($document));
1740 67
        $documentState = $this->getDocumentState($document);
1741
        switch ($documentState) {
1742 67
            case self::STATE_NEW:
1743 67
            case self::STATE_REMOVED:
1744
                // nothing to do
1745 1
                break;
1746 67
            case self::STATE_MANAGED:
1747 67
                $this->lifecycleEventManager->preRemove($class, $document);
1748 67
                $this->scheduleForDelete($document);
1749 67
                break;
1750
            case self::STATE_DETACHED:
1751
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1752
            default:
1753
                throw MongoDBException::invalidDocumentState($documentState);
1754
        }
1755 67
    }
1756
1757
    /**
1758
     * Merges the state of the given detached document into this UnitOfWork.
1759
     *
1760
     * @param object $document
1761
     * @return object The managed copy of the document.
1762
     */
1763 13
    public function merge($document)
1764
    {
1765 13
        $visited = array();
1766
1767 13
        return $this->doMerge($document, $visited);
1768
    }
1769
1770
    /**
1771
     * Executes a merge operation on a document.
1772
     *
1773
     * @param object      $document
1774
     * @param array       $visited
1775
     * @param object|null $prevManagedCopy
1776
     * @param array|null  $assoc
1777
     *
1778
     * @return object The managed copy of the document.
1779
     *
1780
     * @throws InvalidArgumentException If the entity instance is NEW.
1781
     * @throws LockException If the document uses optimistic locking through a
1782
     *                       version attribute and the version check against the
1783
     *                       managed copy fails.
1784
     */
1785 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1786
    {
1787 13
        $oid = spl_object_hash($document);
1788
1789 13
        if (isset($visited[$oid])) {
1790 1
            return $visited[$oid]; // Prevent infinite recursion
1791
        }
1792
1793 13
        $visited[$oid] = $document; // mark visited
1794
1795 13
        $class = $this->dm->getClassMetadata(get_class($document));
1796
1797
        /* First we assume DETACHED, although it can still be NEW but we can
1798
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1799
         * an identity, we need to fetch it from the DB anyway in order to
1800
         * merge. MANAGED documents are ignored by the merge operation.
1801
         */
1802 13
        $managedCopy = $document;
1803
1804 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1805 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1806
                $document->__load();
1807
            }
1808
1809
            // Try to look the document up in the identity map.
1810 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1811
1812 13
            if ($id === null) {
1813
                // If there is no identifier, it is actually NEW.
1814 5
                $managedCopy = $class->newInstance();
1815 5
                $this->persistNew($class, $managedCopy);
1816 5
            } else {
1817 10
                $managedCopy = $this->tryGetById($id, $class);
1818
1819 10
                if ($managedCopy) {
1820
                    // We have the document in memory already, just make sure it is not removed.
1821 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1822
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1823
                    }
1824 5
                } else {
1825
                    // We need to fetch the managed copy in order to merge.
1826 7
                    $managedCopy = $this->dm->find($class->name, $id);
1827
                }
1828
1829 10
                if ($managedCopy === null) {
1830
                    // If the identifier is ASSIGNED, it is NEW
1831
                    $managedCopy = $class->newInstance();
1832
                    $class->setIdentifierValue($managedCopy, $id);
1833
                    $this->persistNew($class, $managedCopy);
1834
                } else {
1835 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...
1836
                        $managedCopy->__load();
1837
                    }
1838
                }
1839
            }
1840
1841 13
            if ($class->isVersioned) {
1842
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1843
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1844
1845
                // Throw exception if versions don't match
1846
                if ($managedCopyVersion != $documentVersion) {
1847
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1848
                }
1849
            }
1850
1851
            // Merge state of $document into existing (managed) document
1852 13
            foreach ($class->reflClass->getProperties() as $prop) {
1853 13
                $name = $prop->name;
1854 13
                $prop->setAccessible(true);
1855 13
                if ( ! isset($class->associationMappings[$name])) {
1856 13
                    if ( ! $class->isIdentifier($name)) {
1857 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
1858 13
                    }
1859 13
                } else {
1860 13
                    $assoc2 = $class->associationMappings[$name];
1861
1862 13
                    if ($assoc2['type'] === 'one') {
1863 5
                        $other = $prop->getValue($document);
1864
1865 5
                        if ($other === null) {
1866 2
                            $prop->setValue($managedCopy, null);
1867 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...
1868
                            // Do not merge fields marked lazy that have not been fetched
1869 1
                            continue;
1870 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1871
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1872
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1873
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1874
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1875
                                $relatedId = $targetClass->getIdentifierObject($other);
1876
1877
                                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...
1878
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1879
                                } else {
1880
                                    $other = $this
1881
                                        ->dm
1882
                                        ->getProxyFactory()
1883
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1884
                                    $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...
1885
                                }
1886
                            }
1887
1888
                            $prop->setValue($managedCopy, $other);
1889
                        }
1890 4
                    } else {
1891 10
                        $mergeCol = $prop->getValue($document);
1892
1893 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
1894
                            /* Do not merge fields marked lazy that have not
1895
                             * been fetched. Keep the lazy persistent collection
1896
                             * of the managed copy.
1897
                             */
1898 3
                            continue;
1899
                        }
1900
1901 7
                        $managedCol = $prop->getValue($managedCopy);
1902
1903 7
                        if ( ! $managedCol) {
1904 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
1905 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1906 2
                            $prop->setValue($managedCopy, $managedCol);
1907 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1908 2
                        }
1909
1910
                        /* Note: do not process association's target documents.
1911
                         * They will be handled during the cascade. Initialize
1912
                         * and, if necessary, clear $managedCol for now.
1913
                         */
1914 7
                        if ($assoc2['isCascadeMerge']) {
1915 7
                            $managedCol->initialize();
1916
1917
                            // If $managedCol differs from the merged collection, clear and set dirty
1918 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1919 2
                                $managedCol->unwrap()->clear();
1920 2
                                $managedCol->setDirty(true);
1921
1922 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1923
                                    $this->scheduleForDirtyCheck($managedCopy);
1924
                                }
1925 2
                            }
1926 7
                        }
1927
                    }
1928
                }
1929
1930 13
                if ($class->isChangeTrackingNotify()) {
1931
                    // Just treat all properties as changed, there is no other choice.
1932
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1933
                }
1934 13
            }
1935
1936 13
            if ($class->isChangeTrackingDeferredExplicit()) {
1937
                $this->scheduleForDirtyCheck($document);
1938
            }
1939 13
        }
1940
1941 13
        if ($prevManagedCopy !== null) {
1942 6
            $assocField = $assoc['fieldName'];
1943 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1944
1945 6
            if ($assoc['type'] === 'one') {
1946 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1947 2
            } else {
1948 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1949
1950 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1951 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1952 1
                }
1953
            }
1954 6
        }
1955
1956
        // Mark the managed copy visited as well
1957 13
        $visited[spl_object_hash($managedCopy)] = true;
1958
1959 13
        $this->cascadeMerge($document, $managedCopy, $visited);
1960
1961 13
        return $managedCopy;
1962
    }
1963
1964
    /**
1965
     * Detaches a document from the persistence management. It's persistence will
1966
     * no longer be managed by Doctrine.
1967
     *
1968
     * @param object $document The document to detach.
1969
     */
1970 9
    public function detach($document)
1971
    {
1972 9
        $visited = array();
1973 9
        $this->doDetach($document, $visited);
1974 9
    }
1975
1976
    /**
1977
     * Executes a detach operation on the given document.
1978
     *
1979
     * @param object $document
1980
     * @param array $visited
1981
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1982
     */
1983 12
    private function doDetach($document, array &$visited)
1984
    {
1985 12
        $oid = spl_object_hash($document);
1986 12
        if (isset($visited[$oid])) {
1987 4
            return; // Prevent infinite recursion
1988
        }
1989
1990 12
        $visited[$oid] = $document; // mark visited
1991
1992 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1993 12
            case self::STATE_MANAGED:
1994 12
                $this->removeFromIdentityMap($document);
1995 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1996 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1997 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1998 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1999 12
                    $this->hasScheduledCollections[$oid]);
2000 12
                break;
2001 4
            case self::STATE_NEW:
2002 4
            case self::STATE_DETACHED:
2003 4
                return;
2004 12
        }
2005
2006 12
        $this->cascadeDetach($document, $visited);
2007 12
    }
2008
2009
    /**
2010
     * Refreshes the state of the given document from the database, overwriting
2011
     * any local, unpersisted changes.
2012
     *
2013
     * @param object $document The document to refresh.
2014
     * @throws \InvalidArgumentException If the document is not MANAGED.
2015
     */
2016 21
    public function refresh($document)
2017
    {
2018 21
        $visited = array();
2019 21
        $this->doRefresh($document, $visited);
2020 20
    }
2021
2022
    /**
2023
     * Executes a refresh operation on a document.
2024
     *
2025
     * @param object $document The document to refresh.
2026
     * @param array $visited The already visited documents during cascades.
2027
     * @throws \InvalidArgumentException If the document is not MANAGED.
2028
     */
2029 21
    private function doRefresh($document, array &$visited)
2030
    {
2031 21
        $oid = spl_object_hash($document);
2032 21
        if (isset($visited[$oid])) {
2033
            return; // Prevent infinite recursion
2034
        }
2035
2036 21
        $visited[$oid] = $document; // mark visited
2037
2038 21
        $class = $this->dm->getClassMetadata(get_class($document));
2039
2040 21
        if ( ! $class->isEmbeddedDocument) {
2041 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2042 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2043 20
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2044 20
            } else {
2045 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2046
            }
2047 20
        }
2048
2049 20
        $this->cascadeRefresh($document, $visited);
2050 20
    }
2051
2052
    /**
2053
     * Cascades a refresh operation to associated documents.
2054
     *
2055
     * @param object $document
2056
     * @param array $visited
2057
     */
2058 20
    private function cascadeRefresh($document, array &$visited)
2059
    {
2060 20
        $class = $this->dm->getClassMetadata(get_class($document));
2061
2062 20
        $associationMappings = array_filter(
2063 20
            $class->associationMappings,
2064
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2065 20
        );
2066
2067 20
        foreach ($associationMappings as $mapping) {
2068 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2069 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2070 15
                if ($relatedDocuments instanceof PersistentCollection) {
2071
                    // Unwrap so that foreach() does not initialize
2072 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2073 15
                }
2074 15
                foreach ($relatedDocuments as $relatedDocument) {
2075
                    $this->doRefresh($relatedDocument, $visited);
2076 15
                }
2077 15
            } elseif ($relatedDocuments !== null) {
2078 2
                $this->doRefresh($relatedDocuments, $visited);
2079 2
            }
2080 20
        }
2081 20
    }
2082
2083
    /**
2084
     * Cascades a detach operation to associated documents.
2085
     *
2086
     * @param object $document
2087
     * @param array $visited
2088
     */
2089 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...
2090
    {
2091 12
        $class = $this->dm->getClassMetadata(get_class($document));
2092 12
        foreach ($class->fieldMappings as $mapping) {
2093 12
            if ( ! $mapping['isCascadeDetach']) {
2094 12
                continue;
2095
            }
2096 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2097 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2098 7
                if ($relatedDocuments instanceof PersistentCollection) {
2099
                    // Unwrap so that foreach() does not initialize
2100 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2101 6
                }
2102 7
                foreach ($relatedDocuments as $relatedDocument) {
2103 5
                    $this->doDetach($relatedDocument, $visited);
2104 7
                }
2105 7
            } elseif ($relatedDocuments !== null) {
2106 5
                $this->doDetach($relatedDocuments, $visited);
2107 5
            }
2108 12
        }
2109 12
    }
2110
    /**
2111
     * Cascades a merge operation to associated documents.
2112
     *
2113
     * @param object $document
2114
     * @param object $managedCopy
2115
     * @param array $visited
2116
     */
2117 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2118
    {
2119 13
        $class = $this->dm->getClassMetadata(get_class($document));
2120
2121 13
        $associationMappings = array_filter(
2122 13
            $class->associationMappings,
2123
            function ($assoc) { return $assoc['isCascadeMerge']; }
2124 13
        );
2125
2126 13
        foreach ($associationMappings as $assoc) {
2127 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2128
2129 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2130 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2131
                    // Collections are the same, so there is nothing to do
2132
                    continue;
2133
                }
2134
2135 8
                if ($relatedDocuments instanceof PersistentCollection) {
2136
                    // Unwrap so that foreach() does not initialize
2137 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2138 6
                }
2139
2140 8
                foreach ($relatedDocuments as $relatedDocument) {
2141 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2142 8
                }
2143 12
            } elseif ($relatedDocuments !== null) {
2144 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2145 3
            }
2146 13
        }
2147 13
    }
2148
2149
    /**
2150
     * Cascades the save operation to associated documents.
2151
     *
2152
     * @param object $document
2153
     * @param array $visited
2154
     */
2155 572
    private function cascadePersist($document, array &$visited)
2156
    {
2157 572
        $class = $this->dm->getClassMetadata(get_class($document));
2158
2159 572
        $associationMappings = array_filter(
2160 572
            $class->associationMappings,
2161
            function ($assoc) { return $assoc['isCascadePersist']; }
2162 572
        );
2163
2164 572
        foreach ($associationMappings as $fieldName => $mapping) {
2165 391
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2166
2167 391
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2168 340
                if ($relatedDocuments instanceof PersistentCollection) {
2169 17
                    if ($relatedDocuments->getOwner() !== $document) {
2170 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2171 2
                    }
2172
                    // Unwrap so that foreach() does not initialize
2173 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2174 17
                }
2175
2176 340
                $count = 0;
2177 340
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2178 187
                    if ( ! empty($mapping['embedded'])) {
2179 113
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2180 113
                        if ($knownParent && $knownParent !== $document) {
2181 4
                            $relatedDocument = clone $relatedDocument;
2182 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2183 4
                        }
2184 113
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2185 113
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2186 113
                    }
2187 187
                    $this->doPersist($relatedDocument, $visited);
2188 339
                }
2189 391
            } elseif ($relatedDocuments !== null) {
2190 120
                if ( ! empty($mapping['embedded'])) {
2191 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2192 66
                    if ($knownParent && $knownParent !== $document) {
2193 5
                        $relatedDocuments = clone $relatedDocuments;
2194 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2195 5
                    }
2196 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2197 66
                }
2198 120
                $this->doPersist($relatedDocuments, $visited);
2199 119
            }
2200 571
        }
2201 570
    }
2202
2203
    /**
2204
     * Cascades the delete operation to associated documents.
2205
     *
2206
     * @param object $document
2207
     * @param array $visited
2208
     */
2209 67 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...
2210
    {
2211 67
        $class = $this->dm->getClassMetadata(get_class($document));
2212 67
        foreach ($class->fieldMappings as $mapping) {
2213 66
            if ( ! $mapping['isCascadeRemove']) {
2214 66
                continue;
2215
            }
2216 33
            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...
2217 2
                $document->__load();
2218 2
            }
2219
2220 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2221 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2222
                // If its a PersistentCollection initialization is intended! No unwrap!
2223 24
                foreach ($relatedDocuments as $relatedDocument) {
2224 13
                    $this->doRemove($relatedDocument, $visited);
2225 24
                }
2226 33
            } elseif ($relatedDocuments !== null) {
2227 12
                $this->doRemove($relatedDocuments, $visited);
2228 12
            }
2229 67
        }
2230 67
    }
2231
2232
    /**
2233
     * Acquire a lock on the given document.
2234
     *
2235
     * @param object $document
2236
     * @param int $lockMode
2237
     * @param int $lockVersion
2238
     * @throws LockException
2239
     * @throws \InvalidArgumentException
2240
     */
2241 9
    public function lock($document, $lockMode, $lockVersion = null)
2242
    {
2243 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2244 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2245
        }
2246
2247 8
        $documentName = get_class($document);
2248 8
        $class = $this->dm->getClassMetadata($documentName);
2249
2250 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2251 3
            if ( ! $class->isVersioned) {
2252 1
                throw LockException::notVersioned($documentName);
2253
            }
2254
2255 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...
2256 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2257 2
                if ($documentVersion != $lockVersion) {
2258 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2259
                }
2260 1
            }
2261 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2262 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2263 5
        }
2264 6
    }
2265
2266
    /**
2267
     * Releases a lock on the given document.
2268
     *
2269
     * @param object $document
2270
     * @throws \InvalidArgumentException
2271
     */
2272 1
    public function unlock($document)
2273
    {
2274 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2275
            throw new \InvalidArgumentException("Document is not MANAGED.");
2276
        }
2277 1
        $documentName = get_class($document);
2278 1
        $this->getDocumentPersister($documentName)->unlock($document);
2279 1
    }
2280
2281
    /**
2282
     * Clears the UnitOfWork.
2283
     *
2284
     * @param string|null $documentName if given, only documents of this type will get detached.
2285
     */
2286 390
    public function clear($documentName = null)
2287
    {
2288 390
        if ($documentName === null) {
2289 384
            $this->identityMap =
2290 384
            $this->documentIdentifiers =
2291 384
            $this->originalDocumentData =
2292 384
            $this->documentChangeSets =
2293 384
            $this->documentStates =
2294 384
            $this->scheduledForDirtyCheck =
2295 384
            $this->documentInsertions =
2296 384
            $this->documentUpserts =
2297 384
            $this->documentUpdates =
2298 384
            $this->documentDeletions =
2299 384
            $this->collectionUpdates =
2300 384
            $this->collectionDeletions =
2301 384
            $this->parentAssociations =
2302 384
            $this->orphanRemovals = 
2303 384
            $this->hasScheduledCollections = array();
2304 384
        } else {
2305 6
            $visited = array();
2306 6
            foreach ($this->identityMap as $className => $documents) {
2307 6
                if ($className === $documentName) {
2308 3
                    foreach ($documents as $document) {
2309 3
                        $this->doDetach($document, $visited);
2310 3
                    }
2311 3
                }
2312 6
            }
2313
        }
2314
2315 390 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...
2316
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2317
        }
2318 390
    }
2319
2320
    /**
2321
     * INTERNAL:
2322
     * Schedules an embedded document for removal. The remove() operation will be
2323
     * invoked on that document at the beginning of the next commit of this
2324
     * UnitOfWork.
2325
     *
2326
     * @ignore
2327
     * @param object $document
2328
     */
2329 48
    public function scheduleOrphanRemoval($document)
2330
    {
2331 48
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2332 48
    }
2333
2334
    /**
2335
     * INTERNAL:
2336
     * Unschedules an embedded or referenced object for removal.
2337
     *
2338
     * @ignore
2339
     * @param object $document
2340
     */
2341 103
    public function unscheduleOrphanRemoval($document)
2342
    {
2343 103
        $oid = spl_object_hash($document);
2344 103
        if (isset($this->orphanRemovals[$oid])) {
2345 1
            unset($this->orphanRemovals[$oid]);
2346 1
        }
2347 103
    }
2348
2349
    /**
2350
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2351
     *  1) sets owner if it was cloned
2352
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2353
     *  3) NOP if state is OK
2354
     * Returned collection should be used from now on (only important with 2nd point)
2355
     *
2356
     * @param PersistentCollection $coll
2357
     * @param object $document
2358
     * @param ClassMetadata $class
2359
     * @param string $propName
2360
     * @return PersistentCollection
2361
     */
2362 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2363
    {
2364 8
        $owner = $coll->getOwner();
2365 8
        if ($owner === null) { // cloned
2366 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2367 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2368 2
            if ( ! $coll->isInitialized()) {
2369 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2370 1
            }
2371 2
            $newValue = clone $coll;
2372 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2373 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2374 2
            if ($this->isScheduledForUpdate($document)) {
2375
                // @todo following line should be superfluous once collections are stored in change sets
2376
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2377
            }
2378 2
            return $newValue;
2379
        }
2380 6
        return $coll;
2381
    }
2382
2383
    /**
2384
     * INTERNAL:
2385
     * Schedules a complete collection for removal when this UnitOfWork commits.
2386
     *
2387
     * @param PersistentCollection $coll
2388
     */
2389 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2390
    {
2391 41
        $oid = spl_object_hash($coll);
2392 41
        unset($this->collectionUpdates[$oid]);
2393 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2394 41
            $this->collectionDeletions[$oid] = $coll;
2395 41
            $this->scheduleCollectionOwner($coll);
2396 41
        }
2397 41
    }
2398
2399
    /**
2400
     * Checks whether a PersistentCollection is scheduled for deletion.
2401
     *
2402
     * @param PersistentCollection $coll
2403
     * @return boolean
2404
     */
2405 106
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2406
    {
2407 106
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2408
    }
2409
    
2410
    /**
2411
     * INTERNAL:
2412
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2413
     * 
2414
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2415
     */
2416 206 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

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

Loading history...
2455
    {
2456 206
        $oid = spl_object_hash($coll);
2457 206
        if (isset($this->collectionUpdates[$oid])) {
2458 196
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2459 196
            unset($this->collectionUpdates[$oid]);
2460 196
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2461 196
        }
2462 206
    }
2463
    
2464
    /**
2465
     * Checks whether a PersistentCollection is scheduled for update.
2466
     *
2467
     * @param PersistentCollection $coll
2468
     * @return boolean
2469
     */
2470 122
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2471
    {
2472 122
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2473
    }
2474
2475
    /**
2476
     * INTERNAL:
2477
     * Gets PersistentCollections that have been visited during computing change
2478
     * set of $document
2479
     *
2480
     * @param object $document
2481
     * @return PersistentCollection[]
2482
     */
2483 540
    public function getVisitedCollections($document)
2484
    {
2485 540
        $oid = spl_object_hash($document);
2486 540
        return isset($this->visitedCollections[$oid])
2487 540
                ? $this->visitedCollections[$oid]
2488 540
                : array();
2489
    }
2490
    
2491
    /**
2492
     * INTERNAL:
2493
     * Gets PersistentCollections that are scheduled to update and related to $document
2494
     * 
2495
     * @param object $document
2496
     * @return array
2497
     */
2498 540
    public function getScheduledCollections($document)
2499
    {
2500 540
        $oid = spl_object_hash($document);
2501 540
        return isset($this->hasScheduledCollections[$oid]) 
2502 540
                ? $this->hasScheduledCollections[$oid]
2503 540
                : array();
2504
    }
2505
    
2506
    /**
2507
     * Checks whether the document is related to a PersistentCollection
2508
     * scheduled for update or deletion.
2509
     *
2510
     * @param object $document
2511
     * @return boolean
2512
     */
2513 77
    public function hasScheduledCollections($document)
2514
    {
2515 77
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2516
    }
2517
    
2518
    /**
2519
     * Marks the PersistentCollection's top-level owner as having a relation to
2520
     * a collection scheduled for update or deletion.
2521
     *
2522
     * If the owner is not scheduled for any lifecycle action, it will be
2523
     * scheduled for update to ensure that versioning takes place if necessary.
2524
     *
2525
     * If the collection is nested within atomic collection, it is immediately
2526
     * unscheduled and atomic one is scheduled for update instead. This makes
2527
     * calculating update data way easier.
2528
     * 
2529
     * @param PersistentCollection $coll
2530
     */
2531 224
    private function scheduleCollectionOwner(PersistentCollection $coll)
2532
    {
2533 224
        $document = $this->getOwningDocument($coll->getOwner());
2534 224
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2535
2536 224
        if ($document !== $coll->getOwner()) {
2537 24
            $parent = $coll->getOwner();
2538 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2539 24
                list($mapping, $parent, ) = $parentAssoc;
2540 24
            }
2541 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2542 7
                $class = $this->dm->getClassMetadata(get_class($document));
2543 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2544 7
                $this->scheduleCollectionUpdate($atomicCollection);
2545 7
                $this->unscheduleCollectionDeletion($coll);
2546 7
                $this->unscheduleCollectionUpdate($coll);
2547 7
            }
2548 24
        }
2549
2550 224
        if ( ! $this->isDocumentScheduled($document)) {
2551 92
            $this->scheduleForUpdate($document);
2552 92
        }
2553 224
    }
2554
2555
    /**
2556
     * Get the top-most owning document of a given document
2557
     *
2558
     * If a top-level document is provided, that same document will be returned.
2559
     * For an embedded document, we will walk through parent associations until
2560
     * we find a top-level document.
2561
     *
2562
     * @param object $document
2563
     * @throws \UnexpectedValueException when a top-level document could not be found
2564
     * @return object
2565
     */
2566 226
    public function getOwningDocument($document)
2567
    {
2568 226
        $class = $this->dm->getClassMetadata(get_class($document));
2569 226
        while ($class->isEmbeddedDocument) {
2570 38
            $parentAssociation = $this->getParentAssociation($document);
2571
2572 38
            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...
2573
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2574
            }
2575
2576 38
            list(, $document, ) = $parentAssociation;
2577 38
            $class = $this->dm->getClassMetadata(get_class($document));
2578 38
        }
2579
2580 226
        return $document;
2581
    }
2582
2583
    /**
2584
     * Gets the class name for an association (embed or reference) with respect
2585
     * to any discriminator value.
2586
     *
2587
     * @param array      $mapping Field mapping for the association
2588
     * @param array|null $data    Data for the embedded document or reference
2589
     */
2590 207
    public function getClassNameForAssociation(array $mapping, $data)
2591
    {
2592 207
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2593
2594 207
        $discriminatorValue = null;
2595 207
        if (isset($discriminatorField, $data[$discriminatorField])) {
2596 21
            $discriminatorValue = $data[$discriminatorField];
2597 207
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2598
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2599
        }
2600
2601 207
        if ($discriminatorValue !== null) {
2602 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2603 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2604 21
                : $discriminatorValue;
2605
        }
2606
2607 187
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2608
2609 187 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...
2610 15
            $discriminatorValue = $data[$class->discriminatorField];
2611 187
        } elseif ($class->defaultDiscriminatorValue !== null) {
2612 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2613 1
        }
2614
2615 187
        if ($discriminatorValue !== null) {
2616 16
            return isset($class->discriminatorMap[$discriminatorValue])
2617 16
                ? $class->discriminatorMap[$discriminatorValue]
2618 16
                : $discriminatorValue;
2619
        }
2620
2621 171
        return $mapping['targetDocument'];
2622
    }
2623
2624
    /**
2625
     * INTERNAL:
2626
     * Creates a document. Used for reconstitution of documents during hydration.
2627
     *
2628
     * @ignore
2629
     * @param string $className The name of the document class.
2630
     * @param array $data The data for the document.
2631
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2632
     * @param object The document to be hydrated into in case of creation
2633
     * @return object The document instance.
2634
     * @internal Highly performance-sensitive method.
2635
     */
2636 385
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2637
    {
2638 385
        $class = $this->dm->getClassMetadata($className);
2639
2640
        // @TODO figure out how to remove this
2641 385
        $discriminatorValue = null;
2642 385 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...
2643 19
            $discriminatorValue = $data[$class->discriminatorField];
2644 385
        } elseif (isset($class->defaultDiscriminatorValue)) {
2645 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2646 2
        }
2647
2648 385
        if ($discriminatorValue !== null) {
2649 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2650 20
                ? $class->discriminatorMap[$discriminatorValue]
2651 20
                : $discriminatorValue;
2652
2653 20
            $class = $this->dm->getClassMetadata($className);
2654
2655 20
            unset($data[$class->discriminatorField]);
2656 20
        }
2657
2658 385
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2659 385
        $serializedId = serialize($id);
2660
2661 385
        if (isset($this->identityMap[$class->name][$serializedId])) {
2662 90
            $document = $this->identityMap[$class->name][$serializedId];
2663 90
            $oid = spl_object_hash($document);
2664 90
            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...
2665 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...
2666 10
                $overrideLocalValues = true;
2667 10
                if ($document instanceof NotifyPropertyChanged) {
2668
                    $document->addPropertyChangedListener($this);
2669
                }
2670 10
            } else {
2671 86
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2672
            }
2673 90
            if ($overrideLocalValues) {
2674 46
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2675 46
                $this->originalDocumentData[$oid] = $data;
2676 46
            }
2677 90
        } else {
2678 357
            if ($document === null) {
2679 357
                $document = $class->newInstance();
2680 357
            }
2681 357
            $this->registerManaged($document, $id, $data);
2682 357
            $oid = spl_object_hash($document);
2683 357
            $this->documentStates[$oid] = self::STATE_MANAGED;
2684 357
            $this->identityMap[$class->name][$serializedId] = $document;
2685 357
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2686 357
            $this->originalDocumentData[$oid] = $data;
2687
        }
2688 385
        return $document;
2689
    }
2690
2691
    /**
2692
     * Initializes (loads) an uninitialized persistent collection of a document.
2693
     *
2694
     * @param PersistentCollection $collection The collection to initialize.
2695
     */
2696 157
    public function loadCollection(PersistentCollection $collection)
2697
    {
2698 157
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2699 157
    }
2700
2701
    /**
2702
     * Gets the identity map of the UnitOfWork.
2703
     *
2704
     * @return array
2705
     */
2706
    public function getIdentityMap()
2707
    {
2708
        return $this->identityMap;
2709
    }
2710
2711
    /**
2712
     * Gets the original data of a document. The original data is the data that was
2713
     * present at the time the document was reconstituted from the database.
2714
     *
2715
     * @param object $document
2716
     * @return array
2717
     */
2718 1
    public function getOriginalDocumentData($document)
2719
    {
2720 1
        $oid = spl_object_hash($document);
2721 1
        if (isset($this->originalDocumentData[$oid])) {
2722 1
            return $this->originalDocumentData[$oid];
2723
        }
2724
        return array();
2725
    }
2726
2727
    /**
2728
     * @ignore
2729
     */
2730 51
    public function setOriginalDocumentData($document, array $data)
2731
    {
2732 51
        $oid = spl_object_hash($document);
2733 51
        $this->originalDocumentData[$oid] = $data;
2734 51
        unset($this->documentChangeSets[$oid]);
2735 51
    }
2736
2737
    /**
2738
     * INTERNAL:
2739
     * Sets a property value of the original data array of a document.
2740
     *
2741
     * @ignore
2742
     * @param string $oid
2743
     * @param string $property
2744
     * @param mixed $value
2745
     */
2746 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2747
    {
2748 3
        $this->originalDocumentData[$oid][$property] = $value;
2749 3
    }
2750
2751
    /**
2752
     * Gets the identifier of a document.
2753
     *
2754
     * @param object $document
2755
     * @return mixed The identifier value
2756
     */
2757 358
    public function getDocumentIdentifier($document)
2758
    {
2759 358
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2760 358
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2761
    }
2762
2763
    /**
2764
     * Checks whether the UnitOfWork has any pending insertions.
2765
     *
2766
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2767
     */
2768
    public function hasPendingInsertions()
2769
    {
2770
        return ! empty($this->documentInsertions);
2771
    }
2772
2773
    /**
2774
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2775
     * number of documents in the identity map.
2776
     *
2777
     * @return integer
2778
     */
2779 2
    public function size()
2780
    {
2781 2
        $count = 0;
2782 2
        foreach ($this->identityMap as $documentSet) {
2783 2
            $count += count($documentSet);
2784 2
        }
2785 2
        return $count;
2786
    }
2787
2788
    /**
2789
     * INTERNAL:
2790
     * Registers a document as managed.
2791
     *
2792
     * TODO: This method assumes that $id is a valid PHP identifier for the
2793
     * document class. If the class expects its database identifier to be a
2794
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2795
     * document identifiers map will become inconsistent with the identity map.
2796
     * In the future, we may want to round-trip $id through a PHP and database
2797
     * conversion and throw an exception if it's inconsistent.
2798
     *
2799
     * @param object $document The document.
2800
     * @param array $id The identifier values.
2801
     * @param array $data The original document data.
2802
     */
2803 379
    public function registerManaged($document, $id, array $data)
2804
    {
2805 379
        $oid = spl_object_hash($document);
2806 379
        $class = $this->dm->getClassMetadata(get_class($document));
2807
2808 379
        if ( ! $class->identifier || $id === null) {
2809 102
            $this->documentIdentifiers[$oid] = $oid;
2810 102
        } else {
2811 373
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2812
        }
2813
2814 379
        $this->documentStates[$oid] = self::STATE_MANAGED;
2815 379
        $this->originalDocumentData[$oid] = $data;
2816 379
        $this->addToIdentityMap($document);
2817 379
    }
2818
2819
    /**
2820
     * INTERNAL:
2821
     * Clears the property changeset of the document with the given OID.
2822
     *
2823
     * @param string $oid The document's OID.
2824
     */
2825 1
    public function clearDocumentChangeSet($oid)
2826
    {
2827 1
        $this->documentChangeSets[$oid] = array();
2828 1
    }
2829
2830
    /* PropertyChangedListener implementation */
2831
2832
    /**
2833
     * Notifies this UnitOfWork of a property change in a document.
2834
     *
2835
     * @param object $document The document that owns the property.
2836
     * @param string $propertyName The name of the property that changed.
2837
     * @param mixed $oldValue The old value of the property.
2838
     * @param mixed $newValue The new value of the property.
2839
     */
2840 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2841
    {
2842 2
        $oid = spl_object_hash($document);
2843 2
        $class = $this->dm->getClassMetadata(get_class($document));
2844
2845 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2846 1
            return; // ignore non-persistent fields
2847
        }
2848
2849
        // Update changeset and mark document for synchronization
2850 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2851 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2852 2
            $this->scheduleForDirtyCheck($document);
2853 2
        }
2854 2
    }
2855
2856
    /**
2857
     * Gets the currently scheduled document insertions in this UnitOfWork.
2858
     *
2859
     * @return array
2860
     */
2861 5
    public function getScheduledDocumentInsertions()
2862
    {
2863 5
        return $this->documentInsertions;
2864
    }
2865
2866
    /**
2867
     * Gets the currently scheduled document upserts in this UnitOfWork.
2868
     *
2869
     * @return array
2870
     */
2871 3
    public function getScheduledDocumentUpserts()
2872
    {
2873 3
        return $this->documentUpserts;
2874
    }
2875
2876
    /**
2877
     * Gets the currently scheduled document updates in this UnitOfWork.
2878
     *
2879
     * @return array
2880
     */
2881 3
    public function getScheduledDocumentUpdates()
2882
    {
2883 3
        return $this->documentUpdates;
2884
    }
2885
2886
    /**
2887
     * Gets the currently scheduled document deletions in this UnitOfWork.
2888
     *
2889
     * @return array
2890
     */
2891
    public function getScheduledDocumentDeletions()
2892
    {
2893
        return $this->documentDeletions;
2894
    }
2895
2896
    /**
2897
     * Get the currently scheduled complete collection deletions
2898
     *
2899
     * @return array
2900
     */
2901
    public function getScheduledCollectionDeletions()
2902
    {
2903
        return $this->collectionDeletions;
2904
    }
2905
2906
    /**
2907
     * Gets the currently scheduled collection inserts, updates and deletes.
2908
     *
2909
     * @return array
2910
     */
2911
    public function getScheduledCollectionUpdates()
2912
    {
2913
        return $this->collectionUpdates;
2914
    }
2915
2916
    /**
2917
     * Helper method to initialize a lazy loading proxy or persistent collection.
2918
     *
2919
     * @param object
2920
     * @return void
2921
     */
2922
    public function initializeObject($obj)
2923
    {
2924
        if ($obj instanceof Proxy) {
2925
            $obj->__load();
2926
        } elseif ($obj instanceof PersistentCollection) {
2927
            $obj->initialize();
2928
        }
2929
    }
2930
2931 1
    private function objToStr($obj)
2932
    {
2933 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2934
    }
2935
}
2936