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

UnitOfWork::setOriginalDocumentData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 5
cts 5
cp 1
rs 9.4286
cc 1
eloc 4
nc 1
nop 2
crap 1
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 552
    public function getDocumentChangeSet($document)
580
    {
581 552
        $oid = spl_object_hash($document);
582 552
        if (isset($this->documentChangeSets[$oid])) {
583 552
            return $this->documentChangeSets[$oid];
584
        }
585 82
        return array();
586
    }
587
588
    /**
589
     * INTERNAL:
590
     * Sets the changeset for a document.
591
     *
592
     * @param object $document
593
     * @param array $changeset
594
     */
595 1
    public function setDocumentChangeSet($document, $changeset)
596
    {
597 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
598 1
    }
599
600
    /**
601
     * Get a documents actual data, flattening all the objects to arrays.
602
     *
603
     * @param object $document
604
     * @return array
605
     */
606 559
    public function getDocumentActualData($document)
607
    {
608 559
        $class = $this->dm->getClassMetadata(get_class($document));
609 559
        $actualData = array();
610 559
        foreach ($class->reflFields as $name => $refProp) {
611 559
            $mapping = $class->fieldMappings[$name];
612
            // skip not saved fields
613 559
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
614 49
                continue;
615
            }
616 559
            $value = $refProp->getValue($document);
617 559
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
618 5
                $value = new GridFSFile($value);
619 5
                $class->reflFields[$name]->setValue($document, $value);
620 5
                $actualData[$name] = $value;
621 559
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
622 559
                && $value !== null && ! ($value instanceof PersistentCollection)) {
623
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
624 366
                if ( ! $value instanceof Collection) {
625 119
                    $value = new ArrayCollection($value);
626 119
                }
627
628
                // Inject PersistentCollection
629 366
                $coll = new PersistentCollection($value, $this->dm, $this);
630 366
                $coll->setOwner($document, $mapping);
631 366
                $coll->setDirty( ! $value->isEmpty());
632 366
                $class->reflFields[$name]->setValue($document, $coll);
633 366
                $actualData[$name] = $coll;
634 366
            } else {
635 559
                $actualData[$name] = $value;
636
            }
637 559
        }
638 559
        return $actualData;
639
    }
640
641
    /**
642
     * Computes the changes that happened to a single document.
643
     *
644
     * Modifies/populates the following properties:
645
     *
646
     * {@link originalDocumentData}
647
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
648
     * then it was not fetched from the database and therefore we have no original
649
     * document data yet. All of the current document data is stored as the original document data.
650
     *
651
     * {@link documentChangeSets}
652
     * The changes detected on all properties of the document are stored there.
653
     * A change is a tuple array where the first entry is the old value and the second
654
     * entry is the new value of the property. Changesets are used by persisters
655
     * to INSERT/UPDATE the persistent document state.
656
     *
657
     * {@link documentUpdates}
658
     * If the document is already fully MANAGED (has been fetched from the database before)
659
     * and any changes to its properties are detected, then a reference to the document is stored
660
     * there to mark it for an update.
661
     *
662
     * @param ClassMetadata $class The class descriptor of the document.
663
     * @param object $document The document for which to compute the changes.
664
     */
665 559
    public function computeChangeSet(ClassMetadata $class, $document)
666
    {
667 559
        if ( ! $class->isInheritanceTypeNone()) {
668 172
            $class = $this->dm->getClassMetadata(get_class($document));
669 172
        }
670
671
        // Fire PreFlush lifecycle callbacks
672 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...
673 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
674 11
        }
675
676 559
        $this->computeOrRecomputeChangeSet($class, $document);
677 558
    }
678
679
    /**
680
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
681
     *
682
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
683
     * @param object $document
684
     * @param boolean $recompute
685
     */
686 559
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
687
    {
688 559
        $oid = spl_object_hash($document);
689 559
        $actualData = $this->getDocumentActualData($document);
690 559
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
691 559
        if ($isNewDocument) {
692
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
693
            // These result in an INSERT.
694 559
            $this->originalDocumentData[$oid] = $actualData;
695 559
            $changeSet = array();
696 559
            foreach ($actualData as $propName => $actualValue) {
697
                /* At this PersistentCollection shouldn't be here, probably it
698
                 * was cloned and its ownership must be fixed
699
                 */
700 559
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
701
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
702
                    $actualValue = $actualData[$propName];
703
                }
704
                // ignore inverse side of reference relationship
705 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...
706 172
                    continue;
707
                }
708 559
                $changeSet[$propName] = array(null, $actualValue);
709 559
            }
710 559
            $this->documentChangeSets[$oid] = $changeSet;
711 559
        } else {
712
            // Document is "fully" MANAGED: it was already fully persisted before
713
            // and we have a copy of the original data
714 279
            $originalData = $this->originalDocumentData[$oid];
715 279
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
716 279
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
717 2
                $changeSet = $this->documentChangeSets[$oid];
718 2
            } else {
719 279
                $changeSet = array();
720
            }
721
722 279
            foreach ($actualData as $propName => $actualValue) {
723
                // skip not saved fields
724 279
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
725
                    continue;
726
                }
727
728 279
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
729
730
                // skip if value has not changed
731 279
                if ($orgValue === $actualValue) {
732
                    // but consider dirty GridFSFile instances as changed
733 278
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
734 278
                        continue;
735
                    }
736 1
                }
737
738
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
739 180
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
740 11
                    if ($orgValue !== null) {
741 6
                        $this->scheduleOrphanRemoval($orgValue);
742 6
                    }
743
744 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
745 11
                    continue;
746
                }
747
748
                // if owning side of reference-one relationship
749 172
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
750 11
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
751 1
                        $this->scheduleOrphanRemoval($orgValue);
752 1
                    }
753
754 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
755 11
                    continue;
756
                }
757
758 164
                if ($isChangeTrackingNotify) {
759 2
                    continue;
760
                }
761
762
                // ignore inverse side of reference relationship
763 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...
764 2
                    continue;
765
                }
766
767
                // Persistent collection was exchanged with the "originally"
768
                // created one. This can only mean it was cloned and replaced
769
                // on another document.
770 163
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
771 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
772 6
                }
773
774
                // if embed-many or reference-many relationship
775 163
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
776 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
777
                    /* If original collection was exchanged with a non-empty value
778
                     * and $set will be issued, there is no need to $unset it first
779
                     */
780 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
781 7
                        continue;
782
                    }
783 19
                    if ($orgValue instanceof PersistentCollection) {
784 17
                        $this->scheduleCollectionDeletion($orgValue);
785 17
                    }
786 19
                    continue;
787
                }
788
789
                // skip equivalent date values
790 149
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
791 36
                    $dateType = Type::getType('date');
792 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
793 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
794
795 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
796 29
                        continue;
797
                    }
798 10
                }
799
800
                // regular field
801 133
                $changeSet[$propName] = array($orgValue, $actualValue);
802 279
            }
803 279
            if ($changeSet) {
804 166
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
805 166
                    ? $changeSet + $this->documentChangeSets[$oid]
806 16
                    : $changeSet;
807
808 166
                $this->originalDocumentData[$oid] = $actualData;
809 166
                $this->scheduleForUpdate($document);
810 166
            }
811
        }
812
813
        // Look for changes in associations of the document
814 559
        $associationMappings = array_filter(
815 559
            $class->associationMappings,
816
            function ($assoc) { return empty($assoc['notSaved']); }
817 559
        );
818
819 559
        foreach ($associationMappings as $mapping) {
820 430
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
821
822 430
            if ($value === null) {
823 290
                continue;
824
            }
825
826 421
            $this->computeAssociationChanges($document, $mapping, $value);
827
828 420
            if (isset($mapping['reference'])) {
829 316
                continue;
830
            }
831
832 328
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
833
834 328
            foreach ($values as $obj) {
835 170
                $oid2 = spl_object_hash($obj);
836
837 170
                if (isset($this->documentChangeSets[$oid2])) {
838 168
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
839
840 168
                    if ( ! $isNewDocument) {
841 72
                        $this->scheduleForUpdate($document);
842 72
                    }
843
844 168
                    break;
845
                }
846 328
            }
847 558
        }
848 558
    }
849
850
    /**
851
     * Computes all the changes that have been done to documents and collections
852
     * since the last commit and stores these changes in the _documentChangeSet map
853
     * temporarily for access by the persisters, until the UoW commit is finished.
854
     */
855 557
    public function computeChangeSets()
856
    {
857 557
        $this->computeScheduleInsertsChangeSets();
858 556
        $this->computeScheduleUpsertsChangeSets();
859
860
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
861 556
        foreach ($this->identityMap as $className => $documents) {
862 556
            $class = $this->dm->getClassMetadata($className);
863 556
            if ($class->isEmbeddedDocument) {
864
                /* we do not want to compute changes to embedded documents up front
865
                 * in case embedded document was replaced and its changeset
866
                 * would corrupt data. Embedded documents' change set will
867
                 * be calculated by reachability from owning document.
868
                 */
869 159
                continue;
870
            }
871
872
            // If change tracking is explicit or happens through notification, then only compute
873
            // changes on document of that type that are explicitly marked for synchronization.
874 556
            switch (true) {
875 556
                case ($class->isChangeTrackingDeferredImplicit()):
876 555
                    $documentsToProcess = $documents;
877 555
                    break;
878
879 3
                case (isset($this->scheduledForDirtyCheck[$className])):
880 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
881 2
                    break;
882
883 3
                default:
884 3
                    $documentsToProcess = array();
885
886 3
            }
887
888 556
            foreach ($documentsToProcess as $document) {
889
                // Ignore uninitialized proxy objects
890 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...
891 10
                    continue;
892
                }
893
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
894 552
                $oid = spl_object_hash($document);
895 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...
896 552
                    && ! isset($this->documentUpserts[$oid])
897 552
                    && ! isset($this->documentDeletions[$oid])
898 552
                    && isset($this->documentStates[$oid])
899 552
                ) {
900 264
                    $this->computeChangeSet($class, $document);
901 264
                }
902 556
            }
903 556
        }
904 556
    }
905
906
    /**
907
     * Computes the changes of an association.
908
     *
909
     * @param object $parentDocument
910
     * @param array $assoc
911
     * @param mixed $value The value of the association.
912
     * @throws \InvalidArgumentException
913
     */
914 421
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
915
    {
916 421
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
917 421
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
918 421
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
919
920 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...
921 8
            return;
922
        }
923
924 420
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
925 226
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
926 222
                $this->scheduleCollectionUpdate($value);
927 222
            }
928 226
            $topmostOwner = $this->getOwningDocument($value->getOwner());
929 226
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
930 226
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
931 132
                $value->initialize();
932 132
                foreach ($value->getDeletedDocuments() as $orphan) {
933 21
                    $this->scheduleOrphanRemoval($orphan);
934 132
                }
935 132
            }
936 226
        }
937
938
        // Look through the documents, and in any of their associations,
939
        // for transient (new) documents, recursively. ("Persistence by reachability")
940
        // Unwrap. Uninitialized collections will simply be empty.
941 420
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
942
943 420
        $count = 0;
944 420
        foreach ($unwrappedValue as $key => $entry) {
945 325
            if ( ! is_object($entry)) {
946 1
                throw new \InvalidArgumentException(
947 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
948 1
                );
949
            }
950
951 324
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
952
953 324
            $state = $this->getDocumentState($entry, self::STATE_NEW);
954
955
            // Handle "set" strategy for multi-level hierarchy
956 324
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
957 324
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
958
959 324
            $count++;
960
961
            switch ($state) {
962 324
                case self::STATE_NEW:
963 57
                    if ( ! $assoc['isCascadePersist']) {
964
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
965
                            . " configured to cascade persist operations: " . $this->objToStr($entry) . "."
966
                            . " Explicitly persist the new document or configure cascading persist operations"
967
                            . " on the relationship.");
968
                    }
969
970 57
                    $this->persistNew($targetClass, $entry);
971 57
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
972 57
                    $this->computeChangeSet($targetClass, $entry);
973 57
                    break;
974
975 319
                case self::STATE_MANAGED:
976 319
                    if ($targetClass->isEmbeddedDocument) {
977 161
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
978 161
                        if ($knownParent && $knownParent !== $parentDocument) {
979 6
                            $entry = clone $entry;
980 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
981 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
982 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
983 3
                            } else {
984
                                // must use unwrapped value to not trigger orphan removal
985 6
                                $unwrappedValue[$key] = $entry;
986
                            }
987 6
                            $this->persistNew($targetClass, $entry);
988 6
                        }
989 161
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
990 161
                        $this->computeChangeSet($targetClass, $entry);
991 161
                    }
992 319
                    break;
993
994 1
                case self::STATE_REMOVED:
995
                    // Consume the $value as array (it's either an array or an ArrayAccess)
996
                    // and remove the element from Collection.
997 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
998
                        unset($value[$key]);
999
                    }
1000 1
                    break;
1001
1002
                case self::STATE_DETACHED:
1003
                    // Can actually not happen right now as we assume STATE_NEW,
1004
                    // so the exception will be raised from the DBAL layer (constraint violation).
1005
                    throw new \InvalidArgumentException("A detached document was found through a "
1006
                        . "relationship during cascading a persist operation.");
1007
1008
                default:
1009
                    // MANAGED associated documents are already taken into account
1010
                    // during changeset calculation anyway, since they are in the identity map.
1011
1012
            }
1013 419
        }
1014 419
    }
1015
1016
    /**
1017
     * INTERNAL:
1018
     * Computes the changeset of an individual document, independently of the
1019
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1020
     *
1021
     * The passed document must be a managed document. If the document already has a change set
1022
     * because this method is invoked during a commit cycle then the change sets are added.
1023
     * whereby changes detected in this method prevail.
1024
     *
1025
     * @ignore
1026
     * @param ClassMetadata $class The class descriptor of the document.
1027
     * @param object $document The document for which to (re)calculate the change set.
1028
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1029
     */
1030 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1031
    {
1032
        // Ignore uninitialized proxy objects
1033 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...
1034 1
            return;
1035
        }
1036
1037 19
        $oid = spl_object_hash($document);
1038
1039 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1040
            throw new \InvalidArgumentException('Document must be managed.');
1041
        }
1042
1043 19
        if ( ! $class->isInheritanceTypeNone()) {
1044 2
            $class = $this->dm->getClassMetadata(get_class($document));
1045 2
        }
1046
1047 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1048 19
    }
1049
1050
    /**
1051
     * @param ClassMetadata $class
1052
     * @param object $document
1053
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1054
     */
1055 577
    private function persistNew(ClassMetadata $class, $document)
1056
    {
1057 577
        $this->lifecycleEventManager->prePersist($class, $document);
1058 577
        $oid = spl_object_hash($document);
1059 577
        $upsert = false;
1060 577
        if ($class->identifier) {
1061 577
            $idValue = $class->getIdentifierValue($document);
1062 577
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1063
1064 577
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1065 3
                throw new \InvalidArgumentException(sprintf(
1066 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1067 3
                    get_class($document)
1068 3
                ));
1069
            }
1070
1071 576
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1072 1
                throw new \InvalidArgumentException(sprintf(
1073 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1074 1
                    get_class($document)
1075 1
                ));
1076
            }
1077
1078 575
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1079 504
                $idValue = $class->idGenerator->generate($this->dm, $document);
1080 504
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1081 504
                $class->setIdentifierValue($document, $idValue);
1082 504
            }
1083
1084 575
            $this->documentIdentifiers[$oid] = $idValue;
1085 575
        } else {
1086
            // this is for embedded documents without identifiers
1087 143
            $this->documentIdentifiers[$oid] = $oid;
1088
        }
1089
1090 575
        $this->documentStates[$oid] = self::STATE_MANAGED;
1091
1092 575
        if ($upsert) {
1093 81
            $this->scheduleForUpsert($class, $document);
1094 81
        } else {
1095 509
            $this->scheduleForInsert($class, $document);
1096
        }
1097 575
    }
1098
1099
    /**
1100
     * Executes all document insertions for documents of the specified type.
1101
     *
1102
     * @param ClassMetadata $class
1103
     * @param array $documents Array of documents to insert
1104
     * @param array $options Array of options to be used with batchInsert()
1105
     */
1106 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...
1107
    {
1108 488
        $persister = $this->getDocumentPersister($class->name);
1109
1110 488
        foreach ($documents as $oid => $document) {
1111 488
            $persister->addInsert($document);
1112 488
            unset($this->documentInsertions[$oid]);
1113 488
        }
1114
1115 488
        $persister->executeInserts($options);
1116
1117 487
        foreach ($documents as $document) {
1118 487
            $this->lifecycleEventManager->postPersist($class, $document);
1119 487
        }
1120 487
    }
1121
1122
    /**
1123
     * Executes all document upserts for documents of the specified type.
1124
     *
1125
     * @param ClassMetadata $class
1126
     * @param array $documents Array of documents to upsert
1127
     * @param array $options Array of options to be used with batchInsert()
1128
     */
1129 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...
1130
    {
1131 78
        $persister = $this->getDocumentPersister($class->name);
1132
1133
1134 78
        foreach ($documents as $oid => $document) {
1135 78
            $persister->addUpsert($document);
1136 78
            unset($this->documentUpserts[$oid]);
1137 78
        }
1138
1139 78
        $persister->executeUpserts($options);
1140
1141 78
        foreach ($documents as $document) {
1142 78
            $this->lifecycleEventManager->postPersist($class, $document);
1143 78
        }
1144 78
    }
1145
1146
    /**
1147
     * Executes all document updates for documents of the specified type.
1148
     *
1149
     * @param Mapping\ClassMetadata $class
1150
     * @param array $documents Array of documents to update
1151
     * @param array $options Array of options to be used with update()
1152
     */
1153 219
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1154
    {
1155 219
        $className = $class->name;
1156 219
        $persister = $this->getDocumentPersister($className);
1157
1158 219
        foreach ($documents as $oid => $document) {
1159 219
            $this->lifecycleEventManager->preUpdate($class, $document);
1160
1161 219
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1162 217
                $persister->update($document, $options);
1163 213
            }
1164
1165 215
            unset($this->documentUpdates[$oid]);
1166
1167 215
            $this->lifecycleEventManager->postUpdate($class, $document);
1168 215
        }
1169 214
    }
1170
1171
    /**
1172
     * Executes all document deletions for documents of the specified type.
1173
     *
1174
     * @param ClassMetadata $class
1175
     * @param array $documents Array of documents to delete
1176
     * @param array $options Array of options to be used with remove()
1177
     */
1178 63
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1179
    {
1180 63
        $persister = $this->getDocumentPersister($class->name);
1181
1182 63
        foreach ($documents as $oid => $document) {
1183 63
            if ( ! $class->isEmbeddedDocument) {
1184 28
                $persister->delete($document, $options);
1185 26
            }
1186
            unset(
1187 61
                $this->documentDeletions[$oid],
1188 61
                $this->documentIdentifiers[$oid],
1189 61
                $this->originalDocumentData[$oid]
1190
            );
1191
1192
            // Clear snapshot information for any referenced PersistentCollection
1193
            // http://www.doctrine-project.org/jira/browse/MODM-95
1194 61
            foreach ($class->associationMappings as $fieldMapping) {
1195 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1196 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1197 26
                    if ($value instanceof PersistentCollection) {
1198 22
                        $value->clearSnapshot();
1199 22
                    }
1200 26
                }
1201 61
            }
1202
1203
            // Document with this $oid after deletion treated as NEW, even if the $oid
1204
            // is obtained by a new document because the old one went out of scope.
1205 61
            $this->documentStates[$oid] = self::STATE_NEW;
1206
1207 61
            $this->lifecycleEventManager->postRemove($class, $document);
1208 61
        }
1209 61
    }
1210
1211
    /**
1212
     * Schedules a document for insertion into the database.
1213
     * If the document already has an identifier, it will be added to the
1214
     * identity map.
1215
     *
1216
     * @param ClassMetadata $class
1217
     * @param object $document The document to schedule for insertion.
1218
     * @throws \InvalidArgumentException
1219
     */
1220 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...
1221
    {
1222 512
        $oid = spl_object_hash($document);
1223
1224 512
        if (isset($this->documentUpdates[$oid])) {
1225
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1226
        }
1227 512
        if (isset($this->documentDeletions[$oid])) {
1228
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1229
        }
1230 512
        if (isset($this->documentInsertions[$oid])) {
1231
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1232
        }
1233
1234 512
        $this->documentInsertions[$oid] = $document;
1235
1236 512
        if (isset($this->documentIdentifiers[$oid])) {
1237 509
            $this->addToIdentityMap($document);
1238 509
        }
1239 512
    }
1240
1241
    /**
1242
     * Schedules a document for upsert into the database and adds it to the
1243
     * identity map
1244
     *
1245
     * @param ClassMetadata $class
1246
     * @param object $document The document to schedule for upsert.
1247
     * @throws \InvalidArgumentException
1248
     */
1249 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1250
    {
1251 84
        $oid = spl_object_hash($document);
1252
1253 84
        if ($class->isEmbeddedDocument) {
1254
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1255
        }
1256 84
        if (isset($this->documentUpdates[$oid])) {
1257
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1258
        }
1259 84
        if (isset($this->documentDeletions[$oid])) {
1260
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1261
        }
1262 84
        if (isset($this->documentUpserts[$oid])) {
1263
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1264
        }
1265
1266 84
        $this->documentUpserts[$oid] = $document;
1267 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1268 84
        $this->addToIdentityMap($document);
1269 84
    }
1270
1271
    /**
1272
     * Checks whether a document is scheduled for insertion.
1273
     *
1274
     * @param object $document
1275
     * @return boolean
1276
     */
1277 100
    public function isScheduledForInsert($document)
1278
    {
1279 100
        return isset($this->documentInsertions[spl_object_hash($document)]);
1280
    }
1281
1282
    /**
1283
     * Checks whether a document is scheduled for upsert.
1284
     *
1285
     * @param object $document
1286
     * @return boolean
1287
     */
1288 5
    public function isScheduledForUpsert($document)
1289
    {
1290 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1291
    }
1292
1293
    /**
1294
     * Schedules a document for being updated.
1295
     *
1296
     * @param object $document The document to schedule for being updated.
1297
     * @throws \InvalidArgumentException
1298
     */
1299 228
    public function scheduleForUpdate($document)
1300
    {
1301 228
        $oid = spl_object_hash($document);
1302 228
        if ( ! isset($this->documentIdentifiers[$oid])) {
1303
            throw new \InvalidArgumentException("Document has no identity.");
1304
        }
1305
1306 228
        if (isset($this->documentDeletions[$oid])) {
1307
            throw new \InvalidArgumentException("Document is removed.");
1308
        }
1309
1310 228
        if ( ! isset($this->documentUpdates[$oid])
1311 228
            && ! isset($this->documentInsertions[$oid])
1312 228
            && ! isset($this->documentUpserts[$oid])) {
1313 224
            $this->documentUpdates[$oid] = $document;
1314 224
        }
1315 228
    }
1316
1317
    /**
1318
     * Checks whether a document is registered as dirty in the unit of work.
1319
     * Note: Is not very useful currently as dirty documents are only registered
1320
     * at commit time.
1321
     *
1322
     * @param object $document
1323
     * @return boolean
1324
     */
1325 13
    public function isScheduledForUpdate($document)
1326
    {
1327 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1328
    }
1329
1330 1
    public function isScheduledForDirtyCheck($document)
1331
    {
1332 1
        $class = $this->dm->getClassMetadata(get_class($document));
1333 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1334
    }
1335
1336
    /**
1337
     * INTERNAL:
1338
     * Schedules a document for deletion.
1339
     *
1340
     * @param object $document
1341
     */
1342 68
    public function scheduleForDelete($document)
1343
    {
1344 68
        $oid = spl_object_hash($document);
1345
1346 68
        if (isset($this->documentInsertions[$oid])) {
1347 2
            if ($this->isInIdentityMap($document)) {
1348 2
                $this->removeFromIdentityMap($document);
1349 2
            }
1350 2
            unset($this->documentInsertions[$oid]);
1351 2
            return; // document has not been persisted yet, so nothing more to do.
1352
        }
1353
1354 67
        if ( ! $this->isInIdentityMap($document)) {
1355 1
            return; // ignore
1356
        }
1357
1358 66
        $this->removeFromIdentityMap($document);
1359 66
        $this->documentStates[$oid] = self::STATE_REMOVED;
1360
1361 66
        if (isset($this->documentUpdates[$oid])) {
1362
            unset($this->documentUpdates[$oid]);
1363
        }
1364 66
        if ( ! isset($this->documentDeletions[$oid])) {
1365 66
            $this->documentDeletions[$oid] = $document;
1366 66
        }
1367 66
    }
1368
1369
    /**
1370
     * Checks whether a document is registered as removed/deleted with the unit
1371
     * of work.
1372
     *
1373
     * @param object $document
1374
     * @return boolean
1375
     */
1376 8
    public function isScheduledForDelete($document)
1377
    {
1378 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1379
    }
1380
1381
    /**
1382
     * Checks whether a document is scheduled for insertion, update or deletion.
1383
     *
1384
     * @param $document
1385
     * @return boolean
1386
     */
1387 225
    public function isDocumentScheduled($document)
1388
    {
1389 225
        $oid = spl_object_hash($document);
1390 225
        return isset($this->documentInsertions[$oid]) ||
1391 121
            isset($this->documentUpserts[$oid]) ||
1392 112
            isset($this->documentUpdates[$oid]) ||
1393 225
            isset($this->documentDeletions[$oid]);
1394
    }
1395
1396
    /**
1397
     * INTERNAL:
1398
     * Registers a document in the identity map.
1399
     *
1400
     * Note that documents in a hierarchy are registered with the class name of
1401
     * the root document. Identifiers are serialized before being used as array
1402
     * keys to allow differentiation of equal, but not identical, values.
1403
     *
1404
     * @ignore
1405
     * @param object $document  The document to register.
1406
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1407
     *                  the document in question is already managed.
1408
     */
1409 606
    public function addToIdentityMap($document)
1410
    {
1411 606
        $class = $this->dm->getClassMetadata(get_class($document));
1412 606
        $id = $this->getIdForIdentityMap($document);
1413
1414 606
        if (isset($this->identityMap[$class->name][$id])) {
1415 53
            return false;
1416
        }
1417
1418 606
        $this->identityMap[$class->name][$id] = $document;
1419
1420 606
        if ($document instanceof NotifyPropertyChanged &&
1421 606
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1422 3
            $document->addPropertyChangedListener($this);
1423 3
        }
1424
1425 606
        return true;
1426
    }
1427
1428
    /**
1429
     * Gets the state of a document with regard to the current unit of work.
1430
     *
1431
     * @param object   $document
1432
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1433
     *                         This parameter can be set to improve performance of document state detection
1434
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1435
     *                         is either known or does not matter for the caller of the method.
1436
     * @return int The document state.
1437
     */
1438 580
    public function getDocumentState($document, $assume = null)
1439
    {
1440 580
        $oid = spl_object_hash($document);
1441
1442 580
        if (isset($this->documentStates[$oid])) {
1443 355
            return $this->documentStates[$oid];
1444
        }
1445
1446 580
        $class = $this->dm->getClassMetadata(get_class($document));
1447
1448 580
        if ($class->isEmbeddedDocument) {
1449 176
            return self::STATE_NEW;
1450
        }
1451
1452 577
        if ($assume !== null) {
1453 574
            return $assume;
1454
        }
1455
1456
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1457
         * known. Note that you cannot remember the NEW or DETACHED state in
1458
         * _documentStates since the UoW does not hold references to such
1459
         * objects and the object hash can be reused. More generally, because
1460
         * the state may "change" between NEW/DETACHED without the UoW being
1461
         * aware of it.
1462
         */
1463 4
        $id = $class->getIdentifierObject($document);
1464
1465 4
        if ($id === null) {
1466 2
            return self::STATE_NEW;
1467
        }
1468
1469
        // Check for a version field, if available, to avoid a DB lookup.
1470 2
        if ($class->isVersioned) {
1471
            return ($class->getFieldValue($document, $class->versionField))
1472
                ? self::STATE_DETACHED
1473
                : self::STATE_NEW;
1474
        }
1475
1476
        // Last try before DB lookup: check the identity map.
1477 2
        if ($this->tryGetById($id, $class)) {
1478 1
            return self::STATE_DETACHED;
1479
        }
1480
1481
        // DB lookup
1482 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1483 1
            return self::STATE_DETACHED;
1484
        }
1485
1486 1
        return self::STATE_NEW;
1487
    }
1488
1489
    /**
1490
     * INTERNAL:
1491
     * Removes a document from the identity map. This effectively detaches the
1492
     * document from the persistence management of Doctrine.
1493
     *
1494
     * @ignore
1495
     * @param object $document
1496
     * @throws \InvalidArgumentException
1497
     * @return boolean
1498
     */
1499 77
    public function removeFromIdentityMap($document)
1500
    {
1501 77
        $oid = spl_object_hash($document);
1502
1503
        // Check if id is registered first
1504 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1505
            return false;
1506
        }
1507
1508 77
        $class = $this->dm->getClassMetadata(get_class($document));
1509 77
        $id = $this->getIdForIdentityMap($document);
1510
1511 77
        if (isset($this->identityMap[$class->name][$id])) {
1512 77
            unset($this->identityMap[$class->name][$id]);
1513 77
            $this->documentStates[$oid] = self::STATE_DETACHED;
1514 77
            return true;
1515
        }
1516
1517
        return false;
1518
    }
1519
1520
    /**
1521
     * INTERNAL:
1522
     * Gets a document in the identity map by its identifier hash.
1523
     *
1524
     * @ignore
1525
     * @param mixed         $id    Document identifier
1526
     * @param ClassMetadata $class Document class
1527
     * @return object
1528
     * @throws InvalidArgumentException if the class does not have an identifier
1529
     */
1530 31
    public function getById($id, ClassMetadata $class)
1531
    {
1532 31
        if ( ! $class->identifier) {
1533
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1534
        }
1535
1536 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1537
1538 31
        return $this->identityMap[$class->name][$serializedId];
1539
    }
1540
1541
    /**
1542
     * INTERNAL:
1543
     * Tries to get a document by its identifier hash. If no document is found
1544
     * for the given hash, FALSE is returned.
1545
     *
1546
     * @ignore
1547
     * @param mixed         $id    Document identifier
1548
     * @param ClassMetadata $class Document class
1549
     * @return mixed The found document or FALSE.
1550
     * @throws InvalidArgumentException if the class does not have an identifier
1551
     */
1552 291
    public function tryGetById($id, ClassMetadata $class)
1553
    {
1554 291
        if ( ! $class->identifier) {
1555
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1556
        }
1557
1558 291
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1559
1560 291
        return isset($this->identityMap[$class->name][$serializedId]) ?
1561 291
            $this->identityMap[$class->name][$serializedId] : false;
1562
    }
1563
1564
    /**
1565
     * Schedules a document for dirty-checking at commit-time.
1566
     *
1567
     * @param object $document The document to schedule for dirty-checking.
1568
     * @todo Rename: scheduleForSynchronization
1569
     */
1570 2
    public function scheduleForDirtyCheck($document)
1571
    {
1572 2
        $class = $this->dm->getClassMetadata(get_class($document));
1573 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1574 2
    }
1575
1576
    /**
1577
     * Checks whether a document is registered in the identity map.
1578
     *
1579
     * @param object $document
1580
     * @return boolean
1581
     */
1582 77
    public function isInIdentityMap($document)
1583
    {
1584 77
        $oid = spl_object_hash($document);
1585
1586 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1587 4
            return false;
1588
        }
1589
1590 76
        $class = $this->dm->getClassMetadata(get_class($document));
1591 76
        $id = $this->getIdForIdentityMap($document);
1592
1593 76
        return isset($this->identityMap[$class->name][$id]);
1594
    }
1595
1596
    /**
1597
     * @param object $document
1598
     * @return string
1599
     */
1600 606
    private function getIdForIdentityMap($document)
1601
    {
1602 606
        $class = $this->dm->getClassMetadata(get_class($document));
1603
1604 606
        if ( ! $class->identifier) {
1605 146
            $id = spl_object_hash($document);
1606 146
        } else {
1607 605
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1608 605
            $id = serialize($class->getDatabaseIdentifierValue($id));
1609
        }
1610
1611 606
        return $id;
1612
    }
1613
1614
    /**
1615
     * INTERNAL:
1616
     * Checks whether an identifier exists in the identity map.
1617
     *
1618
     * @ignore
1619
     * @param string $id
1620
     * @param string $rootClassName
1621
     * @return boolean
1622
     */
1623
    public function containsId($id, $rootClassName)
1624
    {
1625
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1626
    }
1627
1628
    /**
1629
     * Persists a document as part of the current unit of work.
1630
     *
1631
     * @param object $document The document to persist.
1632
     * @throws MongoDBException If trying to persist MappedSuperclass.
1633
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1634
     */
1635 575
    public function persist($document)
1636
    {
1637 575
        $class = $this->dm->getClassMetadata(get_class($document));
1638 575
        if ($class->isMappedSuperclass) {
1639 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1640
        }
1641 574
        $visited = array();
1642 574
        $this->doPersist($document, $visited);
1643 570
    }
1644
1645
    /**
1646
     * Saves a document as part of the current unit of work.
1647
     * This method is internally called during save() cascades as it tracks
1648
     * the already visited documents to prevent infinite recursions.
1649
     *
1650
     * NOTE: This method always considers documents that are not yet known to
1651
     * this UnitOfWork as NEW.
1652
     *
1653
     * @param object $document The document to persist.
1654
     * @param array $visited The already visited documents.
1655
     * @throws \InvalidArgumentException
1656
     * @throws MongoDBException
1657
     */
1658 574
    private function doPersist($document, array &$visited)
1659
    {
1660 574
        $oid = spl_object_hash($document);
1661 574
        if (isset($visited[$oid])) {
1662 24
            return; // Prevent infinite recursion
1663
        }
1664
1665 574
        $visited[$oid] = $document; // Mark visited
1666
1667 574
        $class = $this->dm->getClassMetadata(get_class($document));
1668
1669 574
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1670
        switch ($documentState) {
1671 574
            case self::STATE_MANAGED:
1672
                // Nothing to do, except if policy is "deferred explicit"
1673 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1674
                    $this->scheduleForDirtyCheck($document);
1675
                }
1676 44
                break;
1677 574
            case self::STATE_NEW:
1678 574
                $this->persistNew($class, $document);
1679 572
                break;
1680
1681 2
            case self::STATE_REMOVED:
1682
                // Document becomes managed again
1683 2
                unset($this->documentDeletions[$oid]);
1684
1685 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1686 2
                break;
1687
1688
            case self::STATE_DETACHED:
1689
                throw new \InvalidArgumentException(
1690
                    "Behavior of persist() for a detached document is not yet defined.");
1691
1692
            default:
1693
                throw MongoDBException::invalidDocumentState($documentState);
1694
        }
1695
1696 572
        $this->cascadePersist($document, $visited);
1697 570
    }
1698
1699
    /**
1700
     * Deletes a document as part of the current unit of work.
1701
     *
1702
     * @param object $document The document to remove.
1703
     */
1704 67
    public function remove($document)
1705
    {
1706 67
        $visited = array();
1707 67
        $this->doRemove($document, $visited);
1708 67
    }
1709
1710
    /**
1711
     * Deletes a document as part of the current unit of work.
1712
     *
1713
     * This method is internally called during delete() cascades as it tracks
1714
     * the already visited documents to prevent infinite recursions.
1715
     *
1716
     * @param object $document The document to delete.
1717
     * @param array $visited The map of the already visited documents.
1718
     * @throws MongoDBException
1719
     */
1720 67
    private function doRemove($document, array &$visited)
1721
    {
1722 67
        $oid = spl_object_hash($document);
1723 67
        if (isset($visited[$oid])) {
1724 1
            return; // Prevent infinite recursion
1725
        }
1726
1727 67
        $visited[$oid] = $document; // mark visited
1728
1729
        /* Cascade first, because scheduleForDelete() removes the entity from
1730
         * the identity map, which can cause problems when a lazy Proxy has to
1731
         * be initialized for the cascade operation.
1732
         */
1733 67
        $this->cascadeRemove($document, $visited);
1734
1735 67
        $class = $this->dm->getClassMetadata(get_class($document));
1736 67
        $documentState = $this->getDocumentState($document);
1737
        switch ($documentState) {
1738 67
            case self::STATE_NEW:
1739 67
            case self::STATE_REMOVED:
1740
                // nothing to do
1741 1
                break;
1742 67
            case self::STATE_MANAGED:
1743 67
                $this->lifecycleEventManager->preRemove($class, $document);
1744 67
                $this->scheduleForDelete($document);
1745 67
                break;
1746
            case self::STATE_DETACHED:
1747
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1748
            default:
1749
                throw MongoDBException::invalidDocumentState($documentState);
1750
        }
1751 67
    }
1752
1753
    /**
1754
     * Merges the state of the given detached document into this UnitOfWork.
1755
     *
1756
     * @param object $document
1757
     * @return object The managed copy of the document.
1758
     */
1759 13
    public function merge($document)
1760
    {
1761 13
        $visited = array();
1762
1763 13
        return $this->doMerge($document, $visited);
1764
    }
1765
1766
    /**
1767
     * Executes a merge operation on a document.
1768
     *
1769
     * @param object      $document
1770
     * @param array       $visited
1771
     * @param object|null $prevManagedCopy
1772
     * @param array|null  $assoc
1773
     *
1774
     * @return object The managed copy of the document.
1775
     *
1776
     * @throws InvalidArgumentException If the entity instance is NEW.
1777
     * @throws LockException If the document uses optimistic locking through a
1778
     *                       version attribute and the version check against the
1779
     *                       managed copy fails.
1780
     */
1781 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1782
    {
1783 13
        $oid = spl_object_hash($document);
1784
1785 13
        if (isset($visited[$oid])) {
1786 1
            return $visited[$oid]; // Prevent infinite recursion
1787
        }
1788
1789 13
        $visited[$oid] = $document; // mark visited
1790
1791 13
        $class = $this->dm->getClassMetadata(get_class($document));
1792
1793
        /* First we assume DETACHED, although it can still be NEW but we can
1794
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1795
         * an identity, we need to fetch it from the DB anyway in order to
1796
         * merge. MANAGED documents are ignored by the merge operation.
1797
         */
1798 13
        $managedCopy = $document;
1799
1800 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1801 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1802
                $document->__load();
1803
            }
1804
1805
            // Try to look the document up in the identity map.
1806 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1807
1808 13
            if ($id === null) {
1809
                // If there is no identifier, it is actually NEW.
1810 5
                $managedCopy = $class->newInstance();
1811 5
                $this->persistNew($class, $managedCopy);
1812 5
            } else {
1813 10
                $managedCopy = $this->tryGetById($id, $class);
1814
1815 10
                if ($managedCopy) {
1816
                    // We have the document in memory already, just make sure it is not removed.
1817 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1818
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1819
                    }
1820 5
                } else {
1821
                    // We need to fetch the managed copy in order to merge.
1822 7
                    $managedCopy = $this->dm->find($class->name, $id);
1823
                }
1824
1825 10
                if ($managedCopy === null) {
1826
                    // If the identifier is ASSIGNED, it is NEW
1827
                    $managedCopy = $class->newInstance();
1828
                    $class->setIdentifierValue($managedCopy, $id);
1829
                    $this->persistNew($class, $managedCopy);
1830
                } else {
1831 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...
1832
                        $managedCopy->__load();
1833
                    }
1834
                }
1835
            }
1836
1837 13
            if ($class->isVersioned) {
1838
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1839
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1840
1841
                // Throw exception if versions don't match
1842
                if ($managedCopyVersion != $documentVersion) {
1843
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1844
                }
1845
            }
1846
1847
            // Merge state of $document into existing (managed) document
1848 13
            foreach ($class->reflClass->getProperties() as $prop) {
1849 13
                $name = $prop->name;
1850 13
                $prop->setAccessible(true);
1851 13
                if ( ! isset($class->associationMappings[$name])) {
1852 13
                    if ( ! $class->isIdentifier($name)) {
1853 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
1854 13
                    }
1855 13
                } else {
1856 13
                    $assoc2 = $class->associationMappings[$name];
1857
1858 13
                    if ($assoc2['type'] === 'one') {
1859 5
                        $other = $prop->getValue($document);
1860
1861 5
                        if ($other === null) {
1862 2
                            $prop->setValue($managedCopy, null);
1863 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...
1864
                            // Do not merge fields marked lazy that have not been fetched
1865 1
                            continue;
1866 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
1867
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1868
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
1869
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
1870
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1871
                                $relatedId = $targetClass->getIdentifierObject($other);
1872
1873
                                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...
1874
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1875
                                } else {
1876
                                    $other = $this
1877
                                        ->dm
1878
                                        ->getProxyFactory()
1879
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
1880
                                    $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...
1881
                                }
1882
                            }
1883
1884
                            $prop->setValue($managedCopy, $other);
1885
                        }
1886 4
                    } else {
1887 10
                        $mergeCol = $prop->getValue($document);
1888
1889 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
1890
                            /* Do not merge fields marked lazy that have not
1891
                             * been fetched. Keep the lazy persistent collection
1892
                             * of the managed copy.
1893
                             */
1894 3
                            continue;
1895
                        }
1896
1897 7
                        $managedCol = $prop->getValue($managedCopy);
1898
1899 7
                        if ( ! $managedCol) {
1900 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
1901 2
                            $managedCol->setOwner($managedCopy, $assoc2);
1902 2
                            $prop->setValue($managedCopy, $managedCol);
1903 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1904 2
                        }
1905
1906
                        /* Note: do not process association's target documents.
1907
                         * They will be handled during the cascade. Initialize
1908
                         * and, if necessary, clear $managedCol for now.
1909
                         */
1910 7
                        if ($assoc2['isCascadeMerge']) {
1911 7
                            $managedCol->initialize();
1912
1913
                            // If $managedCol differs from the merged collection, clear and set dirty
1914 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1915 2
                                $managedCol->unwrap()->clear();
1916 2
                                $managedCol->setDirty(true);
1917
1918 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1919
                                    $this->scheduleForDirtyCheck($managedCopy);
1920
                                }
1921 2
                            }
1922 7
                        }
1923
                    }
1924
                }
1925
1926 13
                if ($class->isChangeTrackingNotify()) {
1927
                    // Just treat all properties as changed, there is no other choice.
1928
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1929
                }
1930 13
            }
1931
1932 13
            if ($class->isChangeTrackingDeferredExplicit()) {
1933
                $this->scheduleForDirtyCheck($document);
1934
            }
1935 13
        }
1936
1937 13
        if ($prevManagedCopy !== null) {
1938 6
            $assocField = $assoc['fieldName'];
1939 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1940
1941 6
            if ($assoc['type'] === 'one') {
1942 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1943 2
            } else {
1944 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1945
1946 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1947 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1948 1
                }
1949
            }
1950 6
        }
1951
1952
        // Mark the managed copy visited as well
1953 13
        $visited[spl_object_hash($managedCopy)] = true;
1954
1955 13
        $this->cascadeMerge($document, $managedCopy, $visited);
1956
1957 13
        return $managedCopy;
1958
    }
1959
1960
    /**
1961
     * Detaches a document from the persistence management. It's persistence will
1962
     * no longer be managed by Doctrine.
1963
     *
1964
     * @param object $document The document to detach.
1965
     */
1966 9
    public function detach($document)
1967
    {
1968 9
        $visited = array();
1969 9
        $this->doDetach($document, $visited);
1970 9
    }
1971
1972
    /**
1973
     * Executes a detach operation on the given document.
1974
     *
1975
     * @param object $document
1976
     * @param array $visited
1977
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1978
     */
1979 12
    private function doDetach($document, array &$visited)
1980
    {
1981 12
        $oid = spl_object_hash($document);
1982 12
        if (isset($visited[$oid])) {
1983 4
            return; // Prevent infinite recursion
1984
        }
1985
1986 12
        $visited[$oid] = $document; // mark visited
1987
1988 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1989 12
            case self::STATE_MANAGED:
1990 12
                $this->removeFromIdentityMap($document);
1991 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
1992 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
1993 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
1994 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
1995 12
                    $this->hasScheduledCollections[$oid]);
1996 12
                break;
1997 4
            case self::STATE_NEW:
1998 4
            case self::STATE_DETACHED:
1999 4
                return;
2000 12
        }
2001
2002 12
        $this->cascadeDetach($document, $visited);
2003 12
    }
2004
2005
    /**
2006
     * Refreshes the state of the given document from the database, overwriting
2007
     * any local, unpersisted changes.
2008
     *
2009
     * @param object $document The document to refresh.
2010
     * @throws \InvalidArgumentException If the document is not MANAGED.
2011
     */
2012 21
    public function refresh($document)
2013
    {
2014 21
        $visited = array();
2015 21
        $this->doRefresh($document, $visited);
2016 20
    }
2017
2018
    /**
2019
     * Executes a refresh operation on a document.
2020
     *
2021
     * @param object $document The document to refresh.
2022
     * @param array $visited The already visited documents during cascades.
2023
     * @throws \InvalidArgumentException If the document is not MANAGED.
2024
     */
2025 21
    private function doRefresh($document, array &$visited)
2026
    {
2027 21
        $oid = spl_object_hash($document);
2028 21
        if (isset($visited[$oid])) {
2029
            return; // Prevent infinite recursion
2030
        }
2031
2032 21
        $visited[$oid] = $document; // mark visited
2033
2034 21
        $class = $this->dm->getClassMetadata(get_class($document));
2035
2036 21
        if ( ! $class->isEmbeddedDocument) {
2037 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2038 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2039 20
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2040 20
            } else {
2041 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2042
            }
2043 20
        }
2044
2045 20
        $this->cascadeRefresh($document, $visited);
2046 20
    }
2047
2048
    /**
2049
     * Cascades a refresh operation to associated documents.
2050
     *
2051
     * @param object $document
2052
     * @param array $visited
2053
     */
2054 20
    private function cascadeRefresh($document, array &$visited)
2055
    {
2056 20
        $class = $this->dm->getClassMetadata(get_class($document));
2057
2058 20
        $associationMappings = array_filter(
2059 20
            $class->associationMappings,
2060
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2061 20
        );
2062
2063 20
        foreach ($associationMappings as $mapping) {
2064 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2065 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2066 15
                if ($relatedDocuments instanceof PersistentCollection) {
2067
                    // Unwrap so that foreach() does not initialize
2068 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2069 15
                }
2070 15
                foreach ($relatedDocuments as $relatedDocument) {
2071
                    $this->doRefresh($relatedDocument, $visited);
2072 15
                }
2073 15
            } elseif ($relatedDocuments !== null) {
2074 2
                $this->doRefresh($relatedDocuments, $visited);
2075 2
            }
2076 20
        }
2077 20
    }
2078
2079
    /**
2080
     * Cascades a detach operation to associated documents.
2081
     *
2082
     * @param object $document
2083
     * @param array $visited
2084
     */
2085 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...
2086
    {
2087 12
        $class = $this->dm->getClassMetadata(get_class($document));
2088 12
        foreach ($class->fieldMappings as $mapping) {
2089 12
            if ( ! $mapping['isCascadeDetach']) {
2090 12
                continue;
2091
            }
2092 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2093 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2094 7
                if ($relatedDocuments instanceof PersistentCollection) {
2095
                    // Unwrap so that foreach() does not initialize
2096 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2097 6
                }
2098 7
                foreach ($relatedDocuments as $relatedDocument) {
2099 5
                    $this->doDetach($relatedDocument, $visited);
2100 7
                }
2101 7
            } elseif ($relatedDocuments !== null) {
2102 5
                $this->doDetach($relatedDocuments, $visited);
2103 5
            }
2104 12
        }
2105 12
    }
2106
    /**
2107
     * Cascades a merge operation to associated documents.
2108
     *
2109
     * @param object $document
2110
     * @param object $managedCopy
2111
     * @param array $visited
2112
     */
2113 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2114
    {
2115 13
        $class = $this->dm->getClassMetadata(get_class($document));
2116
2117 13
        $associationMappings = array_filter(
2118 13
            $class->associationMappings,
2119
            function ($assoc) { return $assoc['isCascadeMerge']; }
2120 13
        );
2121
2122 13
        foreach ($associationMappings as $assoc) {
2123 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2124
2125 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2126 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2127
                    // Collections are the same, so there is nothing to do
2128
                    continue;
2129
                }
2130
2131 8
                if ($relatedDocuments instanceof PersistentCollection) {
2132
                    // Unwrap so that foreach() does not initialize
2133 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2134 6
                }
2135
2136 8
                foreach ($relatedDocuments as $relatedDocument) {
2137 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2138 8
                }
2139 12
            } elseif ($relatedDocuments !== null) {
2140 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2141 3
            }
2142 13
        }
2143 13
    }
2144
2145
    /**
2146
     * Cascades the save operation to associated documents.
2147
     *
2148
     * @param object $document
2149
     * @param array $visited
2150
     */
2151 572
    private function cascadePersist($document, array &$visited)
2152
    {
2153 572
        $class = $this->dm->getClassMetadata(get_class($document));
2154
2155 572
        $associationMappings = array_filter(
2156 572
            $class->associationMappings,
2157
            function ($assoc) { return $assoc['isCascadePersist']; }
2158 572
        );
2159
2160 572
        foreach ($associationMappings as $fieldName => $mapping) {
2161 391
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2162
2163 391
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2164 340
                if ($relatedDocuments instanceof PersistentCollection) {
2165 17
                    if ($relatedDocuments->getOwner() !== $document) {
2166 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2167 2
                    }
2168
                    // Unwrap so that foreach() does not initialize
2169 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2170 17
                }
2171
2172 340
                $count = 0;
2173 340
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2174 187
                    if ( ! empty($mapping['embedded'])) {
2175 113
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2176 113
                        if ($knownParent && $knownParent !== $document) {
2177 4
                            $relatedDocument = clone $relatedDocument;
2178 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2179 4
                        }
2180 113
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2181 113
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2182 113
                    }
2183 187
                    $this->doPersist($relatedDocument, $visited);
2184 339
                }
2185 391
            } elseif ($relatedDocuments !== null) {
2186 120
                if ( ! empty($mapping['embedded'])) {
2187 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2188 66
                    if ($knownParent && $knownParent !== $document) {
2189 5
                        $relatedDocuments = clone $relatedDocuments;
2190 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2191 5
                    }
2192 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2193 66
                }
2194 120
                $this->doPersist($relatedDocuments, $visited);
2195 119
            }
2196 571
        }
2197 570
    }
2198
2199
    /**
2200
     * Cascades the delete operation to associated documents.
2201
     *
2202
     * @param object $document
2203
     * @param array $visited
2204
     */
2205 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...
2206
    {
2207 67
        $class = $this->dm->getClassMetadata(get_class($document));
2208 67
        foreach ($class->fieldMappings as $mapping) {
2209 66
            if ( ! $mapping['isCascadeRemove']) {
2210 66
                continue;
2211
            }
2212 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...
2213 2
                $document->__load();
2214 2
            }
2215
2216 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2217 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2218
                // If its a PersistentCollection initialization is intended! No unwrap!
2219 24
                foreach ($relatedDocuments as $relatedDocument) {
2220 13
                    $this->doRemove($relatedDocument, $visited);
2221 24
                }
2222 33
            } elseif ($relatedDocuments !== null) {
2223 12
                $this->doRemove($relatedDocuments, $visited);
2224 12
            }
2225 67
        }
2226 67
    }
2227
2228
    /**
2229
     * Acquire a lock on the given document.
2230
     *
2231
     * @param object $document
2232
     * @param int $lockMode
2233
     * @param int $lockVersion
2234
     * @throws LockException
2235
     * @throws \InvalidArgumentException
2236
     */
2237 9
    public function lock($document, $lockMode, $lockVersion = null)
2238
    {
2239 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2240 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2241
        }
2242
2243 8
        $documentName = get_class($document);
2244 8
        $class = $this->dm->getClassMetadata($documentName);
2245
2246 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2247 3
            if ( ! $class->isVersioned) {
2248 1
                throw LockException::notVersioned($documentName);
2249
            }
2250
2251 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...
2252 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2253 2
                if ($documentVersion != $lockVersion) {
2254 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2255
                }
2256 1
            }
2257 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2258 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2259 5
        }
2260 6
    }
2261
2262
    /**
2263
     * Releases a lock on the given document.
2264
     *
2265
     * @param object $document
2266
     * @throws \InvalidArgumentException
2267
     */
2268 1
    public function unlock($document)
2269
    {
2270 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2271
            throw new \InvalidArgumentException("Document is not MANAGED.");
2272
        }
2273 1
        $documentName = get_class($document);
2274 1
        $this->getDocumentPersister($documentName)->unlock($document);
2275 1
    }
2276
2277
    /**
2278
     * Clears the UnitOfWork.
2279
     *
2280
     * @param string|null $documentName if given, only documents of this type will get detached.
2281
     */
2282 390
    public function clear($documentName = null)
2283
    {
2284 390
        if ($documentName === null) {
2285 384
            $this->identityMap =
2286 384
            $this->documentIdentifiers =
2287 384
            $this->originalDocumentData =
2288 384
            $this->documentChangeSets =
2289 384
            $this->documentStates =
2290 384
            $this->scheduledForDirtyCheck =
2291 384
            $this->documentInsertions =
2292 384
            $this->documentUpserts =
2293 384
            $this->documentUpdates =
2294 384
            $this->documentDeletions =
2295 384
            $this->collectionUpdates =
2296 384
            $this->collectionDeletions =
2297 384
            $this->parentAssociations =
2298 384
            $this->orphanRemovals = 
2299 384
            $this->hasScheduledCollections = array();
2300 384
        } else {
2301 6
            $visited = array();
2302 6
            foreach ($this->identityMap as $className => $documents) {
2303 6
                if ($className === $documentName) {
2304 3
                    foreach ($documents as $document) {
2305 3
                        $this->doDetach($document, $visited);
2306 3
                    }
2307 3
                }
2308 6
            }
2309
        }
2310
2311 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...
2312
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2313
        }
2314 390
    }
2315
2316
    /**
2317
     * INTERNAL:
2318
     * Schedules an embedded document for removal. The remove() operation will be
2319
     * invoked on that document at the beginning of the next commit of this
2320
     * UnitOfWork.
2321
     *
2322
     * @ignore
2323
     * @param object $document
2324
     */
2325 48
    public function scheduleOrphanRemoval($document)
2326
    {
2327 48
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2328 48
    }
2329
2330
    /**
2331
     * INTERNAL:
2332
     * Unschedules an embedded or referenced object for removal.
2333
     *
2334
     * @ignore
2335
     * @param object $document
2336
     */
2337 103
    public function unscheduleOrphanRemoval($document)
2338
    {
2339 103
        $oid = spl_object_hash($document);
2340 103
        if (isset($this->orphanRemovals[$oid])) {
2341 1
            unset($this->orphanRemovals[$oid]);
2342 1
        }
2343 103
    }
2344
2345
    /**
2346
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2347
     *  1) sets owner if it was cloned
2348
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2349
     *  3) NOP if state is OK
2350
     * Returned collection should be used from now on (only important with 2nd point)
2351
     *
2352
     * @param PersistentCollection $coll
2353
     * @param object $document
2354
     * @param ClassMetadata $class
2355
     * @param string $propName
2356
     * @return PersistentCollection
2357
     */
2358 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2359
    {
2360 8
        $owner = $coll->getOwner();
2361 8
        if ($owner === null) { // cloned
2362 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2363 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2364 2
            if ( ! $coll->isInitialized()) {
2365 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2366 1
            }
2367 2
            $newValue = clone $coll;
2368 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2369 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2370 2
            if ($this->isScheduledForUpdate($document)) {
2371
                // @todo following line should be superfluous once collections are stored in change sets
2372
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2373
            }
2374 2
            return $newValue;
2375
        }
2376 6
        return $coll;
2377
    }
2378
2379
    /**
2380
     * INTERNAL:
2381
     * Schedules a complete collection for removal when this UnitOfWork commits.
2382
     *
2383
     * @param PersistentCollection $coll
2384
     */
2385 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2386
    {
2387 41
        $oid = spl_object_hash($coll);
2388 41
        unset($this->collectionUpdates[$oid]);
2389 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2390 41
            $this->collectionDeletions[$oid] = $coll;
2391 41
            $this->scheduleCollectionOwner($coll);
2392 41
        }
2393 41
    }
2394
2395
    /**
2396
     * Checks whether a PersistentCollection is scheduled for deletion.
2397
     *
2398
     * @param PersistentCollection $coll
2399
     * @return boolean
2400
     */
2401 106
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2402
    {
2403 106
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2404
    }
2405
    
2406
    /**
2407
     * INTERNAL:
2408
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2409
     * 
2410
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2411
     */
2412 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...
2413
    {
2414 206
        $oid = spl_object_hash($coll);
2415 206
        if (isset($this->collectionDeletions[$oid])) {
2416 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2417 11
            unset($this->collectionDeletions[$oid]);
2418 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2419 11
        }
2420 206
    }
2421
2422
    /**
2423
     * INTERNAL:
2424
     * Schedules a collection for update when this UnitOfWork commits.
2425
     *
2426
     * @param PersistentCollection $coll
2427
     */
2428 222
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2429
    {
2430 222
        $mapping = $coll->getMapping();
2431 222
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2432
            /* There is no need to $unset collection if it will be $set later
2433
             * This is NOP if collection is not scheduled for deletion
2434
             */
2435 40
            $this->unscheduleCollectionDeletion($coll);
2436 40
        }
2437 222
        $oid = spl_object_hash($coll);
2438 222
        if ( ! isset($this->collectionUpdates[$oid])) {
2439 222
            $this->collectionUpdates[$oid] = $coll;
2440 222
            $this->scheduleCollectionOwner($coll);
2441 222
        }
2442 222
    }
2443
    
2444
    /**
2445
     * INTERNAL:
2446
     * Unschedules a collection from being updated when this UnitOfWork commits.
2447
     * 
2448
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2449
     */
2450 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...
2451
    {
2452 206
        $oid = spl_object_hash($coll);
2453 206
        if (isset($this->collectionUpdates[$oid])) {
2454 196
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2455 196
            unset($this->collectionUpdates[$oid]);
2456 196
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2457 196
        }
2458 206
    }
2459
    
2460
    /**
2461
     * Checks whether a PersistentCollection is scheduled for update.
2462
     *
2463
     * @param PersistentCollection $coll
2464
     * @return boolean
2465
     */
2466 122
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2467
    {
2468 122
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2469
    }
2470
2471
    /**
2472
     * INTERNAL:
2473
     * Gets PersistentCollections that have been visited during computing change
2474
     * set of $document
2475
     *
2476
     * @param object $document
2477
     * @return PersistentCollection[]
2478
     */
2479 540
    public function getVisitedCollections($document)
2480
    {
2481 540
        $oid = spl_object_hash($document);
2482 540
        return isset($this->visitedCollections[$oid])
2483 540
                ? $this->visitedCollections[$oid]
2484 540
                : array();
2485
    }
2486
    
2487
    /**
2488
     * INTERNAL:
2489
     * Gets PersistentCollections that are scheduled to update and related to $document
2490
     * 
2491
     * @param object $document
2492
     * @return array
2493
     */
2494 540
    public function getScheduledCollections($document)
2495
    {
2496 540
        $oid = spl_object_hash($document);
2497 540
        return isset($this->hasScheduledCollections[$oid]) 
2498 540
                ? $this->hasScheduledCollections[$oid]
2499 540
                : array();
2500
    }
2501
    
2502
    /**
2503
     * Checks whether the document is related to a PersistentCollection
2504
     * scheduled for update or deletion.
2505
     *
2506
     * @param object $document
2507
     * @return boolean
2508
     */
2509 77
    public function hasScheduledCollections($document)
2510
    {
2511 77
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2512
    }
2513
    
2514
    /**
2515
     * Marks the PersistentCollection's top-level owner as having a relation to
2516
     * a collection scheduled for update or deletion.
2517
     *
2518
     * If the owner is not scheduled for any lifecycle action, it will be
2519
     * scheduled for update to ensure that versioning takes place if necessary.
2520
     *
2521
     * If the collection is nested within atomic collection, it is immediately
2522
     * unscheduled and atomic one is scheduled for update instead. This makes
2523
     * calculating update data way easier.
2524
     * 
2525
     * @param PersistentCollection $coll
2526
     */
2527 224
    private function scheduleCollectionOwner(PersistentCollection $coll)
2528
    {
2529 224
        $document = $this->getOwningDocument($coll->getOwner());
2530 224
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2531
2532 224
        if ($document !== $coll->getOwner()) {
2533 24
            $parent = $coll->getOwner();
2534 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2535 24
                list($mapping, $parent, ) = $parentAssoc;
2536 24
            }
2537 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2538 7
                $class = $this->dm->getClassMetadata(get_class($document));
2539 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2540 7
                $this->scheduleCollectionUpdate($atomicCollection);
2541 7
                $this->unscheduleCollectionDeletion($coll);
2542 7
                $this->unscheduleCollectionUpdate($coll);
2543 7
            }
2544 24
        }
2545
2546 224
        if ( ! $this->isDocumentScheduled($document)) {
2547 92
            $this->scheduleForUpdate($document);
2548 92
        }
2549 224
    }
2550
2551
    /**
2552
     * Get the top-most owning document of a given document
2553
     *
2554
     * If a top-level document is provided, that same document will be returned.
2555
     * For an embedded document, we will walk through parent associations until
2556
     * we find a top-level document.
2557
     *
2558
     * @param object $document
2559
     * @throws \UnexpectedValueException when a top-level document could not be found
2560
     * @return object
2561
     */
2562 226
    public function getOwningDocument($document)
2563
    {
2564 226
        $class = $this->dm->getClassMetadata(get_class($document));
2565 226
        while ($class->isEmbeddedDocument) {
2566 38
            $parentAssociation = $this->getParentAssociation($document);
2567
2568 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...
2569
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2570
            }
2571
2572 38
            list(, $document, ) = $parentAssociation;
2573 38
            $class = $this->dm->getClassMetadata(get_class($document));
2574 38
        }
2575
2576 226
        return $document;
2577
    }
2578
2579
    /**
2580
     * Gets the class name for an association (embed or reference) with respect
2581
     * to any discriminator value.
2582
     *
2583
     * @param array      $mapping Field mapping for the association
2584
     * @param array|null $data    Data for the embedded document or reference
2585
     */
2586 207
    public function getClassNameForAssociation(array $mapping, $data)
2587
    {
2588 207
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2589
2590 207
        $discriminatorValue = null;
2591 207
        if (isset($discriminatorField, $data[$discriminatorField])) {
2592 21
            $discriminatorValue = $data[$discriminatorField];
2593 207
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2594
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2595
        }
2596
2597 207
        if ($discriminatorValue !== null) {
2598 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2599 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2600 21
                : $discriminatorValue;
2601
        }
2602
2603 187
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2604
2605 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...
2606 15
            $discriminatorValue = $data[$class->discriminatorField];
2607 187
        } elseif ($class->defaultDiscriminatorValue !== null) {
2608 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2609 1
        }
2610
2611 187
        if ($discriminatorValue !== null) {
2612 16
            return isset($class->discriminatorMap[$discriminatorValue])
2613 16
                ? $class->discriminatorMap[$discriminatorValue]
2614 16
                : $discriminatorValue;
2615
        }
2616
2617 171
        return $mapping['targetDocument'];
2618
    }
2619
2620
    /**
2621
     * INTERNAL:
2622
     * Creates a document. Used for reconstitution of documents during hydration.
2623
     *
2624
     * @ignore
2625
     * @param string $className The name of the document class.
2626
     * @param array $data The data for the document.
2627
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2628
     * @param object The document to be hydrated into in case of creation
2629
     * @return object The document instance.
2630
     * @internal Highly performance-sensitive method.
2631
     */
2632 385
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2633
    {
2634 385
        $class = $this->dm->getClassMetadata($className);
2635
2636
        // @TODO figure out how to remove this
2637 385
        $discriminatorValue = null;
2638 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...
2639 19
            $discriminatorValue = $data[$class->discriminatorField];
2640 385
        } elseif (isset($class->defaultDiscriminatorValue)) {
2641 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2642 2
        }
2643
2644 385
        if ($discriminatorValue !== null) {
2645 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2646 20
                ? $class->discriminatorMap[$discriminatorValue]
2647 20
                : $discriminatorValue;
2648
2649 20
            $class = $this->dm->getClassMetadata($className);
2650
2651 20
            unset($data[$class->discriminatorField]);
2652 20
        }
2653
2654 385
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2655 385
        $serializedId = serialize($id);
2656
2657 385
        if (isset($this->identityMap[$class->name][$serializedId])) {
2658 90
            $document = $this->identityMap[$class->name][$serializedId];
2659 90
            $oid = spl_object_hash($document);
2660 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...
2661 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...
2662 10
                $overrideLocalValues = true;
2663 10
                if ($document instanceof NotifyPropertyChanged) {
2664
                    $document->addPropertyChangedListener($this);
2665
                }
2666 10
            } else {
2667 86
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2668
            }
2669 90
            if ($overrideLocalValues) {
2670 46
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2671 46
                $this->originalDocumentData[$oid] = $data;
2672 46
            }
2673 90
        } else {
2674 357
            if ($document === null) {
2675 357
                $document = $class->newInstance();
2676 357
            }
2677 357
            $this->registerManaged($document, $id, $data);
2678 357
            $oid = spl_object_hash($document);
2679 357
            $this->documentStates[$oid] = self::STATE_MANAGED;
2680 357
            $this->identityMap[$class->name][$serializedId] = $document;
2681 357
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2682 357
            $this->originalDocumentData[$oid] = $data;
2683
        }
2684 385
        return $document;
2685
    }
2686
2687
    /**
2688
     * Initializes (loads) an uninitialized persistent collection of a document.
2689
     *
2690
     * @param PersistentCollection $collection The collection to initialize.
2691
     */
2692 157
    public function loadCollection(PersistentCollection $collection)
2693
    {
2694 157
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2695 157
    }
2696
2697
    /**
2698
     * Gets the identity map of the UnitOfWork.
2699
     *
2700
     * @return array
2701
     */
2702
    public function getIdentityMap()
2703
    {
2704
        return $this->identityMap;
2705
    }
2706
2707
    /**
2708
     * Gets the original data of a document. The original data is the data that was
2709
     * present at the time the document was reconstituted from the database.
2710
     *
2711
     * @param object $document
2712
     * @return array
2713
     */
2714 1
    public function getOriginalDocumentData($document)
2715
    {
2716 1
        $oid = spl_object_hash($document);
2717 1
        if (isset($this->originalDocumentData[$oid])) {
2718 1
            return $this->originalDocumentData[$oid];
2719
        }
2720
        return array();
2721
    }
2722
2723
    /**
2724
     * @ignore
2725
     */
2726 51
    public function setOriginalDocumentData($document, array $data)
2727
    {
2728 51
        $oid = spl_object_hash($document);
2729 51
        $this->originalDocumentData[$oid] = $data;
2730 51
        unset($this->documentChangeSets[$oid]);
2731 51
    }
2732
2733
    /**
2734
     * INTERNAL:
2735
     * Sets a property value of the original data array of a document.
2736
     *
2737
     * @ignore
2738
     * @param string $oid
2739
     * @param string $property
2740
     * @param mixed $value
2741
     */
2742 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2743
    {
2744 3
        $this->originalDocumentData[$oid][$property] = $value;
2745 3
    }
2746
2747
    /**
2748
     * Gets the identifier of a document.
2749
     *
2750
     * @param object $document
2751
     * @return mixed The identifier value
2752
     */
2753 358
    public function getDocumentIdentifier($document)
2754
    {
2755 358
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2756 358
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2757
    }
2758
2759
    /**
2760
     * Checks whether the UnitOfWork has any pending insertions.
2761
     *
2762
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2763
     */
2764
    public function hasPendingInsertions()
2765
    {
2766
        return ! empty($this->documentInsertions);
2767
    }
2768
2769
    /**
2770
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2771
     * number of documents in the identity map.
2772
     *
2773
     * @return integer
2774
     */
2775 2
    public function size()
2776
    {
2777 2
        $count = 0;
2778 2
        foreach ($this->identityMap as $documentSet) {
2779 2
            $count += count($documentSet);
2780 2
        }
2781 2
        return $count;
2782
    }
2783
2784
    /**
2785
     * INTERNAL:
2786
     * Registers a document as managed.
2787
     *
2788
     * TODO: This method assumes that $id is a valid PHP identifier for the
2789
     * document class. If the class expects its database identifier to be a
2790
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
2791
     * document identifiers map will become inconsistent with the identity map.
2792
     * In the future, we may want to round-trip $id through a PHP and database
2793
     * conversion and throw an exception if it's inconsistent.
2794
     *
2795
     * @param object $document The document.
2796
     * @param array $id The identifier values.
2797
     * @param array $data The original document data.
2798
     */
2799 379
    public function registerManaged($document, $id, array $data)
2800
    {
2801 379
        $oid = spl_object_hash($document);
2802 379
        $class = $this->dm->getClassMetadata(get_class($document));
2803
2804 379
        if ( ! $class->identifier || $id === null) {
2805 102
            $this->documentIdentifiers[$oid] = $oid;
2806 102
        } else {
2807 373
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2808
        }
2809
2810 379
        $this->documentStates[$oid] = self::STATE_MANAGED;
2811 379
        $this->originalDocumentData[$oid] = $data;
2812 379
        $this->addToIdentityMap($document);
2813 379
    }
2814
2815
    /**
2816
     * INTERNAL:
2817
     * Clears the property changeset of the document with the given OID.
2818
     *
2819
     * @param string $oid The document's OID.
2820
     */
2821 1
    public function clearDocumentChangeSet($oid)
2822
    {
2823 1
        $this->documentChangeSets[$oid] = array();
2824 1
    }
2825
2826
    /* PropertyChangedListener implementation */
2827
2828
    /**
2829
     * Notifies this UnitOfWork of a property change in a document.
2830
     *
2831
     * @param object $document The document that owns the property.
2832
     * @param string $propertyName The name of the property that changed.
2833
     * @param mixed $oldValue The old value of the property.
2834
     * @param mixed $newValue The new value of the property.
2835
     */
2836 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2837
    {
2838 2
        $oid = spl_object_hash($document);
2839 2
        $class = $this->dm->getClassMetadata(get_class($document));
2840
2841 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
2842 1
            return; // ignore non-persistent fields
2843
        }
2844
2845
        // Update changeset and mark document for synchronization
2846 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
2847 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2848 2
            $this->scheduleForDirtyCheck($document);
2849 2
        }
2850 2
    }
2851
2852
    /**
2853
     * Gets the currently scheduled document insertions in this UnitOfWork.
2854
     *
2855
     * @return array
2856
     */
2857 5
    public function getScheduledDocumentInsertions()
2858
    {
2859 5
        return $this->documentInsertions;
2860
    }
2861
2862
    /**
2863
     * Gets the currently scheduled document upserts in this UnitOfWork.
2864
     *
2865
     * @return array
2866
     */
2867 3
    public function getScheduledDocumentUpserts()
2868
    {
2869 3
        return $this->documentUpserts;
2870
    }
2871
2872
    /**
2873
     * Gets the currently scheduled document updates in this UnitOfWork.
2874
     *
2875
     * @return array
2876
     */
2877 3
    public function getScheduledDocumentUpdates()
2878
    {
2879 3
        return $this->documentUpdates;
2880
    }
2881
2882
    /**
2883
     * Gets the currently scheduled document deletions in this UnitOfWork.
2884
     *
2885
     * @return array
2886
     */
2887
    public function getScheduledDocumentDeletions()
2888
    {
2889
        return $this->documentDeletions;
2890
    }
2891
2892
    /**
2893
     * Get the currently scheduled complete collection deletions
2894
     *
2895
     * @return array
2896
     */
2897
    public function getScheduledCollectionDeletions()
2898
    {
2899
        return $this->collectionDeletions;
2900
    }
2901
2902
    /**
2903
     * Gets the currently scheduled collection inserts, updates and deletes.
2904
     *
2905
     * @return array
2906
     */
2907
    public function getScheduledCollectionUpdates()
2908
    {
2909
        return $this->collectionUpdates;
2910
    }
2911
2912
    /**
2913
     * Helper method to initialize a lazy loading proxy or persistent collection.
2914
     *
2915
     * @param object
2916
     * @return void
2917
     */
2918
    public function initializeObject($obj)
2919
    {
2920
        if ($obj instanceof Proxy) {
2921
            $obj->__load();
2922
        } elseif ($obj instanceof PersistentCollection) {
2923
            $obj->initialize();
2924
        }
2925
    }
2926
2927 1
    private function objToStr($obj)
2928
    {
2929 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
2930
    }
2931
}
2932