Completed
Pull Request — master (#1263)
by Andreas
10:33
created

UnitOfWork::getOrCreateDocument()   D

Complexity

Conditions 14
Paths 252

Size

Total Lines 66
Code Lines 43

Duplication

Lines 5
Ratio 7.58 %

Code Coverage

Tests 49
CRAP Score 14.0118

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 5
loc 66
ccs 49
cts 51
cp 0.9608
rs 4.3671
cc 14
eloc 43
nc 252
nop 4
crap 14.0118

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\Common\Collections\ArrayCollection;
23
use Doctrine\Common\Collections\Collection;
24
use Doctrine\Common\EventManager;
25
use Doctrine\Common\NotifyPropertyChanged;
26
use Doctrine\Common\PropertyChangedListener;
27
use Doctrine\MongoDB\GridFSFile;
28
use Doctrine\ODM\MongoDB\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
38
/**
39
 * The UnitOfWork is responsible for tracking changes to objects during an
40
 * "object-level" transaction and for writing out changes to the database
41
 * in the correct order.
42
 *
43
 * @since       1.0
44
 * @author      Jonathan H. Wage <[email protected]>
45
 * @author      Roman Borschel <[email protected]>
46
 */
47
class UnitOfWork implements PropertyChangedListener
48
{
49
    /**
50
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
51
     */
52
    const STATE_MANAGED = 1;
53
54
    /**
55
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
56
     * and is not (yet) managed by a DocumentManager.
57
     */
58
    const STATE_NEW = 2;
59
60
    /**
61
     * A detached document is an instance with a persistent identity that is not
62
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
63
     */
64
    const STATE_DETACHED = 3;
65
66
    /**
67
     * A removed document instance is an instance with a persistent identity,
68
     * associated with a DocumentManager, whose persistent state has been
69
     * deleted (or is scheduled for deletion).
70
     */
71
    const STATE_REMOVED = 4;
72
73
    /**
74
     * The identity map holds references to all managed documents.
75
     *
76
     * Documents are grouped by their class name, and then indexed by the
77
     * serialized string of their database identifier field or, if the class
78
     * has no identifier, the SPL object hash. Serializing the identifier allows
79
     * differentiation of values that may be equal (via type juggling) but not
80
     * identical.
81
     *
82
     * Since all classes in a hierarchy must share the same identifier set,
83
     * we always take the root class name of the hierarchy.
84
     *
85
     * @var array
86
     */
87
    private $identityMap = array();
88
89
    /**
90
     * Map of all identifiers of managed documents.
91
     * Keys are object ids (spl_object_hash).
92
     *
93
     * @var array
94
     */
95
    private $documentIdentifiers = array();
96
97
    /**
98
     * Map of the original document data of managed documents.
99
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
100
     * at commit time.
101
     *
102
     * @var array
103
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
104
     *           A value will only really be copied if the value in the document is modified
105
     *           by the user.
106
     */
107
    private $originalDocumentData = array();
108
109
    /**
110
     * Map of document changes. Keys are object ids (spl_object_hash).
111
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
112
     *
113
     * @var array
114
     */
115
    private $documentChangeSets = array();
116
117
    /**
118
     * The (cached) states of any known documents.
119
     * Keys are object ids (spl_object_hash).
120
     *
121
     * @var array
122
     */
123
    private $documentStates = array();
124
125
    /**
126
     * Map of documents that are scheduled for dirty checking at commit time.
127
     *
128
     * Documents are grouped by their class name, and then indexed by their SPL
129
     * object hash. This is only used for documents with a change tracking
130
     * policy of DEFERRED_EXPLICIT.
131
     *
132
     * @var array
133
     * @todo rename: scheduledForSynchronization
134
     */
135
    private $scheduledForDirtyCheck = array();
136
137
    /**
138
     * A list of all pending document insertions.
139
     *
140
     * @var array
141
     */
142
    private $documentInsertions = array();
143
144
    /**
145
     * A list of all pending document updates.
146
     *
147
     * @var array
148
     */
149
    private $documentUpdates = array();
150
151
    /**
152
     * A list of all pending document upserts.
153
     *
154
     * @var array
155
     */
156
    private $documentUpserts = array();
157
158
    /**
159
     * A list of all pending document deletions.
160
     *
161
     * @var array
162
     */
163
    private $documentDeletions = array();
164
165
    /**
166
     * All pending collection deletions.
167
     *
168
     * @var array
169
     */
170
    private $collectionDeletions = array();
171
172
    /**
173
     * All pending collection updates.
174
     *
175
     * @var array
176
     */
177
    private $collectionUpdates = array();
178
    
179
    /**
180
     * A list of documents related to collections scheduled for update or deletion
181
     * 
182
     * @var array
183
     */
184
    private $hasScheduledCollections = array();
185
186
    /**
187
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
188
     * At the end of the UnitOfWork all these collections will make new snapshots
189
     * of their data.
190
     *
191
     * @var array
192
     */
193
    private $visitedCollections = array();
194
195
    /**
196
     * The DocumentManager that "owns" this UnitOfWork instance.
197
     *
198
     * @var DocumentManager
199
     */
200
    private $dm;
201
202
    /**
203
     * The EventManager used for dispatching events.
204
     *
205
     * @var EventManager
206
     */
207
    private $evm;
208
209
    /**
210
     * Additional documents that are scheduled for removal.
211
     *
212
     * @var array
213
     */
214
    private $orphanRemovals = array();
215
216
    /**
217
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
218
     *
219
     * @var HydratorFactory
220
     */
221
    private $hydratorFactory;
222
223
    /**
224
     * The document persister instances used to persist document instances.
225
     *
226
     * @var array
227
     */
228
    private $persisters = array();
229
230
    /**
231
     * The collection persister instance used to persist changes to collections.
232
     *
233
     * @var Persisters\CollectionPersister
234
     */
235
    private $collectionPersister;
236
237
    /**
238
     * The persistence builder instance used in DocumentPersisters.
239
     *
240
     * @var PersistenceBuilder
241
     */
242
    private $persistenceBuilder;
243
244
    /**
245
     * Array of parent associations between embedded documents
246
     *
247
     * @todo We might need to clean up this array in clear(), doDetach(), etc.
248
     * @var array
249
     */
250
    private $parentAssociations = array();
251
252
    /**
253
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
254
     *
255
     * @param DocumentManager $dm
256
     * @param EventManager $evm
257
     * @param HydratorFactory $hydratorFactory
258
     */
259 933
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
260
    {
261 933
        $this->dm = $dm;
262 933
        $this->evm = $evm;
263 933
        $this->hydratorFactory = $hydratorFactory;
264 933
    }
265
266
    /**
267
     * Factory for returning new PersistenceBuilder instances used for preparing data into
268
     * queries for insert persistence.
269
     *
270
     * @return PersistenceBuilder $pb
271
     */
272 676
    public function getPersistenceBuilder()
273
    {
274 676
        if ( ! $this->persistenceBuilder) {
275 676
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
276 676
        }
277 676
        return $this->persistenceBuilder;
278
    }
279
280
    /**
281
     * Sets the parent association for a given embedded document.
282
     *
283
     * @param object $document
284
     * @param array $mapping
285
     * @param object $parent
286
     * @param string $propertyPath
287
     */
288 181
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
289
    {
290 181
        $oid = spl_object_hash($document);
291 181
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
292 181
    }
293
294
    /**
295
     * Gets the parent association for a given embedded document.
296
     *
297
     *     <code>
298
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
299
     *     </code>
300
     *
301
     * @param object $document
302
     * @return array $association
303
     */
304 207
    public function getParentAssociation($document)
305
    {
306 207
        $oid = spl_object_hash($document);
307 207
        if ( ! isset($this->parentAssociations[$oid])) {
308 203
            return null;
309
        }
310 165
        return $this->parentAssociations[$oid];
311
    }
312
313
    /**
314
     * Get the document persister instance for the given document name
315
     *
316
     * @param string $documentName
317
     * @return Persisters\DocumentPersister
318
     */
319 674
    public function getDocumentPersister($documentName)
320
    {
321 674
        if ( ! isset($this->persisters[$documentName])) {
322 660
            $class = $this->dm->getClassMetadata($documentName);
323 660
            $pb = $this->getPersistenceBuilder();
324 660
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
325 660
        }
326 674
        return $this->persisters[$documentName];
327
    }
328
329
    /**
330
     * Get the collection persister instance.
331
     *
332
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
333
     */
334 674
    public function getCollectionPersister()
335
    {
336 674
        if ( ! isset($this->collectionPersister)) {
337 674
            $pb = $this->getPersistenceBuilder();
338 674
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
339 674
        }
340 674
        return $this->collectionPersister;
341
    }
342
343
    /**
344
     * Set the document persister instance to use for the given document name
345
     *
346
     * @param string $documentName
347
     * @param Persisters\DocumentPersister $persister
348
     */
349 14
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
350
    {
351 14
        $this->persisters[$documentName] = $persister;
352 14
    }
353
354
    /**
355
     * Commits the UnitOfWork, executing all operations that have been postponed
356
     * up to this point. The state of all managed documents will be synchronized with
357
     * the database.
358
     *
359
     * The operations are executed in the following order:
360
     *
361
     * 1) All document insertions
362
     * 2) All document updates
363
     * 3) All document deletions
364
     *
365
     * @param object $document
366
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
367
     */
368 560
    public function commit($document = null, array $options = array())
369
    {
370
        // Raise preFlush
371 560
        if ($this->evm->hasListeners(Events::preFlush)) {
372
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
373
        }
374
375 560
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
376 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...
377
            $options = array_merge($defaultOptions, $options);
378
        } else {
379 560
            $options = $defaultOptions;
380
        }
381
        // Compute changes done since last commit.
382 560
        if ($document === null) {
383 554
            $this->computeChangeSets();
384 559
        } elseif (is_object($document)) {
385 12
            $this->computeSingleDocumentChangeSet($document);
386 12
        } elseif (is_array($document)) {
387 1
            foreach ($document as $object) {
388 1
                $this->computeSingleDocumentChangeSet($object);
389 1
            }
390 1
        }
391
392 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...
393 238
            $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...
394 201
            $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...
395 191
            $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...
396 23
            $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...
397 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...
398 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...
399 558
        ) {
400 23
            return; // Nothing to do.
401
        }
402
403 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...
404 46
            foreach ($this->orphanRemovals as $removal) {
405 46
                $this->remove($removal);
406 46
            }
407 46
        }
408
409
        // Raise onFlush
410 555
        if ($this->evm->hasListeners(Events::onFlush)) {
411 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
412 7
        }
413
414 555
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
415 78
            list($class, $documents) = $classAndDocuments;
416 78
            $this->executeUpserts($class, $documents, $options);
417 555
        }
418
419 555
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
420 488
            list($class, $documents) = $classAndDocuments;
421 488
            $this->executeInserts($class, $documents, $options);
422 554
        }
423
424 554
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
425 218
            list($class, $documents) = $classAndDocuments;
426 218
            $this->executeUpdates($class, $documents, $options);
427 554
        }
428
429 554
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
430 63
            list($class, $documents) = $classAndDocuments;
431 63
            $this->executeDeletions($class, $documents, $options);
432 554
        }
433
434
        // Raise postFlush
435 554
        if ($this->evm->hasListeners(Events::postFlush)) {
436
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
437
        }
438
439
        // Clear up
440 554
        $this->documentInsertions =
441 554
        $this->documentUpserts =
442 554
        $this->documentUpdates =
443 554
        $this->documentDeletions =
444 554
        $this->documentChangeSets =
445 554
        $this->collectionUpdates =
446 554
        $this->collectionDeletions =
447 554
        $this->visitedCollections =
448 554
        $this->scheduledForDirtyCheck =
449 554
        $this->orphanRemovals = 
450 554
        $this->hasScheduledCollections = array();
451 554
    }
452
453
    /**
454
     * Groups a list of scheduled documents by their class.
455
     *
456
     * @param array $documents Scheduled documents (e.g. $this->documentInsertions)
457
     * @param bool $includeEmbedded
458
     * @return array Tuples of ClassMetadata and a corresponding array of objects
459
     */
460 555
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
461
    {
462 555
        if (empty($documents)) {
463 555
            return array();
464
        }
465 554
        $divided = array();
466 554
        $embeds = array();
467 554
        foreach ($documents as $oid => $d) {
468 554
            $className = get_class($d);
469 554
            if (isset($embeds[$className])) {
470 68
                continue;
471
            }
472 554
            if (isset($divided[$className])) {
473 136
                $divided[$className][1][$oid] = $d;
474 136
                continue;
475
            }
476 554
            $class = $this->dm->getClassMetadata($className);
477 554
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
478 166
                $embeds[$className] = true;
479 166
                continue;
480
            }
481 554
            if (empty($divided[$class->name])) {
482 554
                $divided[$class->name] = array($class, array($oid => $d));
483 554
            } else {
484 4
                $divided[$class->name][1][$oid] = $d;
485
            }
486 554
        }
487 554
        return $divided;
488
    }
489
490
    /**
491
     * Compute changesets of all documents scheduled for insertion.
492
     *
493
     * Embedded documents will not be processed.
494
     */
495 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...
496
    {
497 562
        foreach ($this->documentInsertions as $document) {
498 496
            $class = $this->dm->getClassMetadata(get_class($document));
499 496
            if ( ! $class->isEmbeddedDocument) {
500 493
                $this->computeChangeSet($class, $document);
501 492
            }
502 561
        }
503 561
    }
504
505
    /**
506
     * Compute changesets of all documents scheduled for upsert.
507
     *
508
     * Embedded documents will not be processed.
509
     */
510 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...
511
    {
512 561
        foreach ($this->documentUpserts as $document) {
513 77
            $class = $this->dm->getClassMetadata(get_class($document));
514 77
            if ( ! $class->isEmbeddedDocument) {
515 77
                $this->computeChangeSet($class, $document);
516 77
            }
517 561
        }
518 561
    }
519
520
    /**
521
     * Only flush the given document according to a ruleset that keeps the UoW consistent.
522
     *
523
     * 1. All documents scheduled for insertion and (orphan) removals are processed as well!
524
     * 2. Proxies are skipped.
525
     * 3. Only if document is properly managed.
526
     *
527
     * @param  object $document
528
     * @throws \InvalidArgumentException If the document is not STATE_MANAGED
529
     * @return void
530
     */
531 13
    private function computeSingleDocumentChangeSet($document)
532
    {
533 13
        $state = $this->getDocumentState($document);
534
535 13
        if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) {
536 1
            throw new \InvalidArgumentException("Document has to be managed or scheduled for removal for single computation " . $this->objToStr($document));
537
        }
538
539 12
        $class = $this->dm->getClassMetadata(get_class($document));
540
541 12
        if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) {
542 9
            $this->persist($document);
543 9
        }
544
545
        // Compute changes for INSERTed and UPSERTed documents first. This must always happen even in this case.
546 12
        $this->computeScheduleInsertsChangeSets();
547 12
        $this->computeScheduleUpsertsChangeSets();
548
549
        // Ignore uninitialized proxy objects
550 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...
551
            return;
552
        }
553
554
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
555 12
        $oid = spl_object_hash($document);
556
557 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...
558 12
            && ! isset($this->documentUpserts[$oid])
559 12
            && ! isset($this->documentDeletions[$oid])
560 12
            && isset($this->documentStates[$oid])
561 12
        ) {
562 8
            $this->computeChangeSet($class, $document);
563 8
        }
564 12
    }
565
566
    /**
567
     * Gets the changeset for a document.
568
     *
569
     * @param object $document
570
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
571
     */
572 545
    public function getDocumentChangeSet($document)
573
    {
574 545
        $oid = spl_object_hash($document);
575 545
        if (isset($this->documentChangeSets[$oid])) {
576 545
            return $this->documentChangeSets[$oid];
577
        }
578 27
        return array();
579
    }
580
581
    /**
582
     * Get a documents actual data, flattening all the objects to arrays.
583
     *
584
     * @param object $document
585
     * @return array
586
     */
587 559
    public function getDocumentActualData($document)
588
    {
589 559
        $class = $this->dm->getClassMetadata(get_class($document));
590 559
        $actualData = array();
591 559
        foreach ($class->reflFields as $name => $refProp) {
592 559
            $mapping = $class->fieldMappings[$name];
593
            // skip not saved fields
594 559
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
595 49
                continue;
596
            }
597 559
            $value = $refProp->getValue($document);
598 559
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
599 5
                $value = new GridFSFile($value);
600 5
                $class->reflFields[$name]->setValue($document, $value);
601 5
                $actualData[$name] = $value;
602 559
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
603 559
                && $value !== null && ! ($value instanceof PersistentCollection)) {
604
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
605 367
                if ( ! $value instanceof Collection) {
606 120
                    $value = new ArrayCollection($value);
607 120
                }
608
609
                // Inject PersistentCollection
610 367
                $coll = new PersistentCollection($value, $this->dm, $this);
611 367
                $coll->setOwner($document, $mapping);
612 367
                $coll->setDirty( ! $value->isEmpty());
613 367
                $class->reflFields[$name]->setValue($document, $coll);
614 367
                $actualData[$name] = $coll;
615 367
            } else {
616 559
                $actualData[$name] = $value;
617
            }
618 559
        }
619 559
        return $actualData;
620
    }
621
622
    /**
623
     * Computes the changes that happened to a single document.
624
     *
625
     * Modifies/populates the following properties:
626
     *
627
     * {@link originalDocumentData}
628
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
629
     * then it was not fetched from the database and therefore we have no original
630
     * document data yet. All of the current document data is stored as the original document data.
631
     *
632
     * {@link documentChangeSets}
633
     * The changes detected on all properties of the document are stored there.
634
     * A change is a tuple array where the first entry is the old value and the second
635
     * entry is the new value of the property. Changesets are used by persisters
636
     * to INSERT/UPDATE the persistent document state.
637
     *
638
     * {@link documentUpdates}
639
     * If the document is already fully MANAGED (has been fetched from the database before)
640
     * and any changes to its properties are detected, then a reference to the document is stored
641
     * there to mark it for an update.
642
     *
643
     * @param ClassMetadata $class The class descriptor of the document.
644
     * @param object $document The document for which to compute the changes.
645
     */
646 559
    public function computeChangeSet(ClassMetadata $class, $document)
647
    {
648 559
        if ( ! $class->isInheritanceTypeNone()) {
649 172
            $class = $this->dm->getClassMetadata(get_class($document));
650 172
        }
651
652
        // Fire PreFlush lifecycle callbacks
653 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...
654 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, array(new Event\PreFlushEventArgs($this->dm)));
655 11
        }
656
657 559
        $this->computeOrRecomputeChangeSet($class, $document);
658 558
    }
659
660
    /**
661
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
662
     *
663
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
664
     * @param object $document
665
     * @param boolean $recompute
666
     */
667 559
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
668
    {
669 559
        $oid = spl_object_hash($document);
670 559
        $actualData = $this->getDocumentActualData($document);
671 559
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
672 559
        if ($isNewDocument) {
673
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
674
            // These result in an INSERT.
675 559
            $this->originalDocumentData[$oid] = $actualData;
676 559
            $changeSet = array();
677 559
            foreach ($actualData as $propName => $actualValue) {
678
                /* At this PersistentCollection shouldn't be here, probably it
679
                 * was cloned and its ownership must be fixed
680
                 */
681 559
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
682
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
683
                    $actualValue = $actualData[$propName];
684
                }
685
                // ignore inverse side of reference relationship
686 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...
687 173
                    continue;
688
                }
689 559
                $changeSet[$propName] = array(null, $actualValue);
690 559
            }
691 559
            $this->documentChangeSets[$oid] = $changeSet;
692 559
        } else {
693
            // Document is "fully" MANAGED: it was already fully persisted before
694
            // and we have a copy of the original data
695 278
            $originalData = $this->originalDocumentData[$oid];
696 278
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
697 278
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
698 2
                $changeSet = $this->documentChangeSets[$oid];
699 2
            } else {
700 278
                $changeSet = array();
701
            }
702
703 278
            foreach ($actualData as $propName => $actualValue) {
704
                // skip not saved fields
705 278
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
706
                    continue;
707
                }
708
709 278
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
710
711
                // skip if value has not changed
712 278
                if ($orgValue === $actualValue) {
713
                    // but consider dirty GridFSFile instances as changed
714 277
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
715 277
                        continue;
716
                    }
717 1
                }
718
719
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
720 179
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
721 11
                    if ($orgValue !== null) {
722 6
                        $this->scheduleOrphanRemoval($orgValue);
723 6
                    }
724
725 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
726 11
                    continue;
727
                }
728
729
                // if owning side of reference-one relationship
730 171
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
731 11
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
732 1
                        $this->scheduleOrphanRemoval($orgValue);
733 1
                    }
734
735 11
                    $changeSet[$propName] = array($orgValue, $actualValue);
736 11
                    continue;
737
                }
738
739 163
                if ($isChangeTrackingNotify) {
740 2
                    continue;
741
                }
742
743
                // ignore inverse side of reference relationship
744 162 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...
745 2
                    continue;
746
                }
747
748
                // Persistent collection was exchanged with the "originally"
749
                // created one. This can only mean it was cloned and replaced
750
                // on another document.
751 162
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
752 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
753 6
                }
754
755
                // if embed-many or reference-many relationship
756 162
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
757 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
758
                    /* If original collection was exchanged with a non-empty value
759
                     * and $set will be issued, there is no need to $unset it first
760
                     */
761 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
762 7
                        continue;
763
                    }
764 19
                    if ($orgValue instanceof PersistentCollection) {
765 17
                        $this->scheduleCollectionDeletion($orgValue);
766 17
                    }
767 19
                    continue;
768
                }
769
770
                // skip equivalent date values
771 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
772 36
                    $dateType = Type::getType('date');
773 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
774 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
775
776 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
777 29
                        continue;
778
                    }
779 10
                }
780
781
                // regular field
782 132
                $changeSet[$propName] = array($orgValue, $actualValue);
783 278
            }
784 278
            if ($changeSet) {
785 165
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
786 165
                    ? $changeSet + $this->documentChangeSets[$oid]
787 17
                    : $changeSet;
788
789 165
                $this->originalDocumentData[$oid] = $actualData;
790 165
                $this->scheduleForUpdate($document);
791 165
            }
792
        }
793
794
        // Look for changes in associations of the document
795 559
        $associationMappings = array_filter(
796 559
            $class->associationMappings,
797
            function ($assoc) { return empty($assoc['notSaved']); }
798 559
        );
799
800 559
        foreach ($associationMappings as $mapping) {
801 431
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
802
803 431
            if ($value === null) {
804 291
                continue;
805
            }
806
807 422
            $this->computeAssociationChanges($document, $mapping, $value);
808
809 421
            if (isset($mapping['reference'])) {
810 317
                continue;
811
            }
812
813 328
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
814
815 328
            foreach ($values as $obj) {
816 170
                $oid2 = spl_object_hash($obj);
817
818 170
                if (isset($this->documentChangeSets[$oid2])) {
819 168
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
820
821 168
                    if ( ! $isNewDocument) {
822 72
                        $this->scheduleForUpdate($document);
823 72
                    }
824
825 168
                    break;
826
                }
827 328
            }
828 558
        }
829 558
    }
830
831
    /**
832
     * Computes all the changes that have been done to documents and collections
833
     * since the last commit and stores these changes in the _documentChangeSet map
834
     * temporarily for access by the persisters, until the UoW commit is finished.
835
     */
836 557
    public function computeChangeSets()
837
    {
838 557
        $this->computeScheduleInsertsChangeSets();
839 556
        $this->computeScheduleUpsertsChangeSets();
840
841
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
842 556
        foreach ($this->identityMap as $className => $documents) {
843 556
            $class = $this->dm->getClassMetadata($className);
844 556
            if ($class->isEmbeddedDocument) {
845
                /* we do not want to compute changes to embedded documents up front
846
                 * in case embedded document was replaced and its changeset
847
                 * would corrupt data. Embedded documents' change set will
848
                 * be calculated by reachability from owning document.
849
                 */
850 159
                continue;
851
            }
852
853
            // If change tracking is explicit or happens through notification, then only compute
854
            // changes on document of that type that are explicitly marked for synchronization.
855 556
            switch (true) {
856 556
                case ($class->isChangeTrackingDeferredImplicit()):
857 555
                    $documentsToProcess = $documents;
858 555
                    break;
859
860 3
                case (isset($this->scheduledForDirtyCheck[$className])):
861 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
862 2
                    break;
863
864 3
                default:
865 3
                    $documentsToProcess = array();
866
867 3
            }
868
869 556
            foreach ($documentsToProcess as $document) {
870
                // Ignore uninitialized proxy objects
871 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...
872 10
                    continue;
873
                }
874
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
875 552
                $oid = spl_object_hash($document);
876 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...
877 552
                    && ! isset($this->documentUpserts[$oid])
878 552
                    && ! isset($this->documentDeletions[$oid])
879 552
                    && isset($this->documentStates[$oid])
880 552
                ) {
881 263
                    $this->computeChangeSet($class, $document);
882 263
                }
883 556
            }
884 556
        }
885 556
    }
886
887
    /**
888
     * Computes the changes of an association.
889
     *
890
     * @param object $parentDocument
891
     * @param array $assoc
892
     * @param mixed $value The value of the association.
893
     * @throws \InvalidArgumentException
894
     */
895 422
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
896
    {
897 422
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
898 422
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
899 422
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
900
901 422
        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...
902 8
            return;
903
        }
904
905 421
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
906 227
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
907 223
                $this->scheduleCollectionUpdate($value);
908 223
            }
909 227
            $topmostOwner = $this->getOwningDocument($value->getOwner());
910 227
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
911 227
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
912 132
                $value->initialize();
913 132
                foreach ($value->getDeletedDocuments() as $orphan) {
914 21
                    $this->scheduleOrphanRemoval($orphan);
915 132
                }
916 132
            }
917 227
        }
918
919
        // Look through the documents, and in any of their associations,
920
        // for transient (new) documents, recursively. ("Persistence by reachability")
921
        // Unwrap. Uninitialized collections will simply be empty.
922 421
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
923
924 421
        $count = 0;
925 421
        foreach ($unwrappedValue as $key => $entry) {
926 326
            if ( ! is_object($entry)) {
927 1
                throw new \InvalidArgumentException(
928 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
929 1
                );
930
            }
931
932 325
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
933
934 325
            $state = $this->getDocumentState($entry, self::STATE_NEW);
935
936
            // Handle "set" strategy for multi-level hierarchy
937 325
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
938 325
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
939
940 325
            $count++;
941
942
            switch ($state) {
943 325
                case self::STATE_NEW:
944 57
                    if ( ! $assoc['isCascadePersist']) {
945
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
946
                            . " configured to cascade persist operations: " . $this->objToStr($entry) . "."
947
                            . " Explicitly persist the new document or configure cascading persist operations"
948
                            . " on the relationship.");
949
                    }
950
951 57
                    $this->persistNew($targetClass, $entry);
952 57
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
953 57
                    $this->computeChangeSet($targetClass, $entry);
954 57
                    break;
955
956 320
                case self::STATE_MANAGED:
957 320
                    if ($targetClass->isEmbeddedDocument) {
958 161
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
959 161
                        if ($knownParent && $knownParent !== $parentDocument) {
960 6
                            $entry = clone $entry;
961 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
962 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
963 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
964 3
                            } else {
965
                                // must use unwrapped value to not trigger orphan removal
966 6
                                $unwrappedValue[$key] = $entry;
967
                            }
968 6
                            $this->persistNew($targetClass, $entry);
969 6
                        }
970 161
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
971 161
                        $this->computeChangeSet($targetClass, $entry);
972 161
                    }
973 320
                    break;
974
975 1
                case self::STATE_REMOVED:
976
                    // Consume the $value as array (it's either an array or an ArrayAccess)
977
                    // and remove the element from Collection.
978 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
979
                        unset($value[$key]);
980
                    }
981 1
                    break;
982
983
                case self::STATE_DETACHED:
984
                    // Can actually not happen right now as we assume STATE_NEW,
985
                    // so the exception will be raised from the DBAL layer (constraint violation).
986
                    throw new \InvalidArgumentException("A detached document was found through a "
987
                        . "relationship during cascading a persist operation.");
988
989
                default:
990
                    // MANAGED associated documents are already taken into account
991
                    // during changeset calculation anyway, since they are in the identity map.
992
993
            }
994 420
        }
995 420
    }
996
997
    /**
998
     * INTERNAL:
999
     * Computes the changeset of an individual document, independently of the
1000
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1001
     *
1002
     * The passed document must be a managed document. If the document already has a change set
1003
     * because this method is invoked during a commit cycle then the change sets are added.
1004
     * whereby changes detected in this method prevail.
1005
     *
1006
     * @ignore
1007
     * @param ClassMetadata $class The class descriptor of the document.
1008
     * @param object $document The document for which to (re)calculate the change set.
1009
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1010
     */
1011 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1012
    {
1013
        // Ignore uninitialized proxy objects
1014 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...
1015 1
            return;
1016
        }
1017
1018 19
        $oid = spl_object_hash($document);
1019
1020 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1021
            throw new \InvalidArgumentException('Document must be managed.');
1022
        }
1023
1024 19
        if ( ! $class->isInheritanceTypeNone()) {
1025 2
            $class = $this->dm->getClassMetadata(get_class($document));
1026 2
        }
1027
1028 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1029 19
    }
1030
1031
    /**
1032
     * @param ClassMetadata $class
1033
     * @param object $document
1034
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1035
     */
1036 577
    private function persistNew(ClassMetadata $class, $document)
1037
    {
1038 577
        $oid = spl_object_hash($document);
1039 577 View Code Duplication
        if ( ! empty($class->lifecycleCallbacks[Events::prePersist])) {
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...
1040 157
            $class->invokeLifecycleCallbacks(Events::prePersist, $document, array(new LifecycleEventArgs($document, $this->dm)));
1041 157
        }
1042 577 View Code Duplication
        if ($this->evm->hasListeners(Events::prePersist)) {
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...
1043 6
            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($document, $this->dm));
1044 6
        }
1045
1046 577
        $upsert = false;
1047 577
        if ($class->identifier) {
1048 577
            $idValue = $class->getIdentifierValue($document);
1049 577
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1050
1051 577
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1052 3
                throw new \InvalidArgumentException(sprintf(
1053 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1054 3
                    get_class($document)
1055 3
                ));
1056
            }
1057
1058 576
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! \MongoId::isValid($idValue)) {
1059 1
                throw new \InvalidArgumentException(sprintf(
1060 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1061 1
                    get_class($document)
1062 1
                ));
1063
            }
1064
1065 575
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1066 504
                $idValue = $class->idGenerator->generate($this->dm, $document);
1067 504
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1068 504
                $class->setIdentifierValue($document, $idValue);
1069 504
            }
1070
1071 575
            $this->documentIdentifiers[$oid] = $idValue;
1072 575
        } else {
1073
            // this is for embedded documents without identifiers
1074 143
            $this->documentIdentifiers[$oid] = $oid;
1075
        }
1076
1077 575
        $this->documentStates[$oid] = self::STATE_MANAGED;
1078
1079 575
        if ($upsert) {
1080 81
            $this->scheduleForUpsert($class, $document);
1081 81
        } else {
1082 509
            $this->scheduleForInsert($class, $document);
1083
        }
1084 575
    }
1085
1086
    /**
1087
     * Cascades the postPersist events to embedded documents.
1088
     *
1089
     * @param ClassMetadata $class
1090
     * @param object $document
1091
     */
1092 553
    private function cascadePostPersist(ClassMetadata $class, $document)
1093
    {
1094 553
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1095
1096 553
        $embeddedMappings = array_filter(
1097 553
            $class->associationMappings,
1098
            function($assoc) { return ! empty($assoc['embedded']); }
1099 553
        );
1100
1101 553
        foreach ($embeddedMappings as $mapping) {
1102 336
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1103
1104 336
            if ($value === null) {
1105 215
                continue;
1106
            }
1107
1108 317
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1109
1110 317
            if (isset($mapping['targetDocument'])) {
1111 306
                $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1112 306
            }
1113
1114 317
            foreach ($values as $embeddedDocument) {
1115 159
                if ( ! isset($mapping['targetDocument'])) {
1116 13
                    $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument));
1117 13
                }
1118
1119 159 View Code Duplication
                if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) {
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...
1120 9
                    $embeddedClass->invokeLifecycleCallbacks(Events::postPersist, $embeddedDocument, array(new LifecycleEventArgs($document, $this->dm)));
0 ignored issues
show
Bug introduced by
The variable $embeddedClass does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1121 9
                }
1122 159
                if ($hasPostPersistListeners) {
1123 4
                    $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm));
1124 4
                }
1125 159
                $this->cascadePostPersist($embeddedClass, $embeddedDocument);
1126 317
            }
1127 553
         }
1128 553
     }
1129
1130
    /**
1131
     * Executes all document insertions for documents of the specified type.
1132
     *
1133
     * @param ClassMetadata $class
1134
     * @param array $documents Array of documents to insert
1135
     * @param array $options Array of options to be used with batchInsert()
1136
     */
1137 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...
1138
    {
1139 488
        $persister = $this->getDocumentPersister($class->name);
1140
1141 488
        foreach ($documents as $oid => $document) {
1142 488
            $persister->addInsert($document);
1143 488
            unset($this->documentInsertions[$oid]);
1144 488
        }
1145
1146 488
        $persister->executeInserts($options);
1147
1148 487
        $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]);
1149 487
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1150
1151 487
        foreach ($documents as $document) {
1152 487
            if ($hasPostPersistLifecycleCallbacks) {
1153 10
                $class->invokeLifecycleCallbacks(Events::postPersist, $document, array(new LifecycleEventArgs($document, $this->dm)));
1154 10
            }
1155 487
            if ($hasPostPersistListeners) {
1156 5
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1157 5
            }
1158 487
            $this->cascadePostPersist($class, $document);
1159 487
        }
1160 487
    }
1161
1162
    /**
1163
     * Executes all document upserts for documents of the specified type.
1164
     *
1165
     * @param ClassMetadata $class
1166
     * @param array $documents Array of documents to upsert
1167
     * @param array $options Array of options to be used with batchInsert()
1168
     */
1169 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...
1170
    {
1171 78
        $persister = $this->getDocumentPersister($class->name);
1172
1173
1174 78
        foreach ($documents as $oid => $document) {
1175 78
            $persister->addUpsert($document);
1176 78
            unset($this->documentUpserts[$oid]);
1177 78
        }
1178
1179 78
        $persister->executeUpserts($options);
1180
1181 78
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
1182 78
        $hasListeners = $this->evm->hasListeners(Events::postPersist);
1183
1184 78
        foreach ($documents as $document) {
1185 78
            if ($hasLifecycleCallbacks) {
1186
                $class->invokeLifecycleCallbacks(Events::postPersist, $document, array(new LifecycleEventArgs($document, $this->dm)));
1187
            }
1188 78
            if ($hasListeners) {
1189 2
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1190 2
            }
1191 78
            $this->cascadePostPersist($class, $document);
1192 78
        }
1193 78
    }
1194
1195
    /**
1196
     * Executes all document updates for documents of the specified type.
1197
     *
1198
     * @param Mapping\ClassMetadata $class
1199
     * @param array $documents Array of documents to update
1200
     * @param array $options Array of options to be used with update()
1201
     */
1202 218
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1203
    {
1204 218
        $className = $class->name;
1205 218
        $persister = $this->getDocumentPersister($className);
1206
1207 218
        $hasPreUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::preUpdate]);
1208 218
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1209 218
        $hasPostUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postUpdate]);
1210 218
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1211
1212 218
        foreach ($documents as $oid => $document) {
1213 218
            if ( ! isset($this->documentChangeSets[$oid])) {
1214
                // only ReferenceMany collection is scheduled for update
1215 60
                $this->documentChangeSets[$oid] = array();
1216 60
            }
1217 218 View Code Duplication
            if ($hasPreUpdateLifecycleCallbacks) {
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...
1218 12
                $class->invokeLifecycleCallbacks(Events::preUpdate, $document, array(
1219 12
                    new Event\PreUpdateEventArgs($document, $this->dm, $this->documentChangeSets[$oid])
1220 12
                ));
1221 12
                $this->recomputeSingleDocumentChangeSet($class, $document);
1222 12
            }
1223
1224 218 View Code Duplication
            if ($hasPreUpdateListeners) {
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...
1225 8
                $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1226 8
                    $document, $this->dm, $this->documentChangeSets[$oid])
1227 8
                );
1228 8
            }
1229 218
            $this->cascadePreUpdate($class, $document);
1230
1231 218
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1232 216
                $persister->update($document, $options);
1233 212
            }
1234
1235 214
            unset($this->documentUpdates[$oid]);
1236
1237 214
            if ($hasPostUpdateLifecycleCallbacks) {
1238 7
                $class->invokeLifecycleCallbacks(Events::postUpdate, $document, array(new LifecycleEventArgs($document, $this->dm)));
1239 7
            }
1240 214
            if ($hasPostUpdateListeners) {
1241 8
                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm));
1242 8
            }
1243 214
            $this->cascadePostUpdate($class, $document);
1244 214
        }
1245 213
    }
1246
1247
    /**
1248
     * Cascades the preUpdate event to embedded documents.
1249
     *
1250
     * @param ClassMetadata $class
1251
     * @param object $document
1252
     */
1253 218
    private function cascadePreUpdate(ClassMetadata $class, $document)
1254
    {
1255 218
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1256
1257 218
        $embeddedMappings = array_filter(
1258 218
            $class->associationMappings,
1259
            function ($assoc) { return ! empty($assoc['embedded']); }
1260 218
        );
1261
1262 218
        foreach ($embeddedMappings as $mapping) {
1263 134
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1264
1265 134
            if ($value === null) {
1266 49
                continue;
1267
            }
1268
1269 132
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1270
1271 132
            foreach ($values as $entry) {
1272 85
                $entryOid = spl_object_hash($entry);
1273 85
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1274
1275 85
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1276 47
                    continue;
1277
                }
1278
1279 68
                if (isset($this->documentInsertions[$entryOid])) {
1280 53
                    continue;
1281
                }
1282
1283 45 View Code Duplication
                if ( ! empty($entryClass->lifecycleCallbacks[Events::preUpdate])) {
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...
1284 5
                    $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry, array(
1285 5
                        new Event\PreUpdateEventArgs($entry, $this->dm, $this->documentChangeSets[$entryOid])
1286 5
                    ));
1287 5
                    $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
1288 5
                }
1289 45 View Code Duplication
                if ($hasPreUpdateListeners) {
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...
1290 3
                    $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1291 3
                        $entry, $this->dm, $this->documentChangeSets[$entryOid])
1292 3
                    );
1293 3
                }
1294
1295 45
                $this->cascadePreUpdate($entryClass, $entry);
1296 132
            }
1297 218
        }
1298 218
    }
1299
1300
    /**
1301
     * Cascades the postUpdate and postPersist events to embedded documents.
1302
     *
1303
     * @param ClassMetadata $class
1304
     * @param object $document
1305
     */
1306 214
    private function cascadePostUpdate(ClassMetadata $class, $document)
1307
    {
1308 214
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1309 214
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1310
1311 214
        $embeddedMappings = array_filter(
1312 214
            $class->associationMappings,
1313
            function($assoc) { return ! empty($assoc['embedded']); }
1314 214
        );
1315
1316 214
        foreach ($embeddedMappings as $mapping) {
1317 130
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1318
1319 130
            if ($value === null) {
1320 52
                continue;
1321
            }
1322
1323 128
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1324
1325 128
            foreach ($values as $entry) {
1326 85
                $entryOid = spl_object_hash($entry);
1327 85
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1328
1329 85
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1330 47
                    continue;
1331
                }
1332
1333 68
                if (isset($this->documentInsertions[$entryOid])) {
1334 53 View Code Duplication
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) {
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...
1335 1
                        $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry, array(
1336 1
                            new LifecycleEventArgs($entry, $this->dm)
1337 1
                        ));
1338 1
                    }
1339 53
                    if ($hasPostPersistListeners) {
1340 3
                        $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm));
1341 3
                    }
1342 53
                } else {
1343 45 View Code Duplication
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) {
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...
1344 9
                        $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry, array(
1345 9
                            new LifecycleEventArgs($entry, $this->dm)
1346 9
                        ));
1347 9
                    }
1348 45
                    if ($hasPostUpdateListeners) {
1349 3
                        $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm));
1350 3
                    }
1351
                }
1352
1353 68
                $this->cascadePostUpdate($entryClass, $entry);
1354 128
            }
1355 214
        }
1356 214
    }
1357
1358
    /**
1359
     * Executes all document deletions for documents of the specified type.
1360
     *
1361
     * @param ClassMetadata $class
1362
     * @param array $documents Array of documents to delete
1363
     * @param array $options Array of options to be used with remove()
1364
     */
1365 63
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1366
    {
1367 63
        $hasPostRemoveLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postRemove]);
1368 63
        $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove);
1369
1370 63
        $persister = $this->getDocumentPersister($class->name);
1371
1372 63
        foreach ($documents as $oid => $document) {
1373 63
            if ( ! $class->isEmbeddedDocument) {
1374 28
                $persister->delete($document, $options);
1375 26
            }
1376
            unset(
1377 61
                $this->documentDeletions[$oid],
1378 61
                $this->documentIdentifiers[$oid],
1379 61
                $this->originalDocumentData[$oid]
1380
            );
1381
1382
            // Clear snapshot information for any referenced PersistentCollection
1383
            // http://www.doctrine-project.org/jira/browse/MODM-95
1384 61
            foreach ($class->associationMappings as $fieldMapping) {
1385 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1386 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1387 26
                    if ($value instanceof PersistentCollection) {
1388 22
                        $value->clearSnapshot();
1389 22
                    }
1390 26
                }
1391 61
            }
1392
1393
            // Document with this $oid after deletion treated as NEW, even if the $oid
1394
            // is obtained by a new document because the old one went out of scope.
1395 61
            $this->documentStates[$oid] = self::STATE_NEW;
1396
1397 61
            if ($hasPostRemoveLifecycleCallbacks) {
1398 8
                $class->invokeLifecycleCallbacks(Events::postRemove, $document, array(new LifecycleEventArgs($document, $this->dm)));
1399 8
            }
1400 61
            if ($hasPostRemoveListeners) {
1401 2
                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm));
1402 2
            }
1403 61
        }
1404 61
    }
1405
1406
    /**
1407
     * Schedules a document for insertion into the database.
1408
     * If the document already has an identifier, it will be added to the
1409
     * identity map.
1410
     *
1411
     * @param ClassMetadata $class
1412
     * @param object $document The document to schedule for insertion.
1413
     * @throws \InvalidArgumentException
1414
     */
1415 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...
1416
    {
1417 512
        $oid = spl_object_hash($document);
1418
1419 512
        if (isset($this->documentUpdates[$oid])) {
1420
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1421
        }
1422 512
        if (isset($this->documentDeletions[$oid])) {
1423
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1424
        }
1425 512
        if (isset($this->documentInsertions[$oid])) {
1426
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1427
        }
1428
1429 512
        $this->documentInsertions[$oid] = $document;
1430
1431 512
        if (isset($this->documentIdentifiers[$oid])) {
1432 509
            $this->addToIdentityMap($document);
1433 509
        }
1434 512
    }
1435
1436
    /**
1437
     * Schedules a document for upsert into the database and adds it to the
1438
     * identity map
1439
     *
1440
     * @param ClassMetadata $class
1441
     * @param object $document The document to schedule for upsert.
1442
     * @throws \InvalidArgumentException
1443
     */
1444 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1445
    {
1446 84
        $oid = spl_object_hash($document);
1447
1448 84
        if ($class->isEmbeddedDocument) {
1449
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1450
        }
1451 84
        if (isset($this->documentUpdates[$oid])) {
1452
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1453
        }
1454 84
        if (isset($this->documentDeletions[$oid])) {
1455
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1456
        }
1457 84
        if (isset($this->documentUpserts[$oid])) {
1458
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1459
        }
1460
1461 84
        $this->documentUpserts[$oid] = $document;
1462 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1463 84
        $this->addToIdentityMap($document);
1464 84
    }
1465
1466
    /**
1467
     * Checks whether a document is scheduled for insertion.
1468
     *
1469
     * @param object $document
1470
     * @return boolean
1471
     */
1472 71
    public function isScheduledForInsert($document)
1473
    {
1474 71
        return isset($this->documentInsertions[spl_object_hash($document)]);
1475
    }
1476
1477
    /**
1478
     * Checks whether a document is scheduled for upsert.
1479
     *
1480
     * @param object $document
1481
     * @return boolean
1482
     */
1483 5
    public function isScheduledForUpsert($document)
1484
    {
1485 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1486
    }
1487
1488
    /**
1489
     * Schedules a document for being updated.
1490
     *
1491
     * @param object $document The document to schedule for being updated.
1492
     * @throws \InvalidArgumentException
1493
     */
1494 227
    public function scheduleForUpdate($document)
1495
    {
1496 227
        $oid = spl_object_hash($document);
1497 227
        if ( ! isset($this->documentIdentifiers[$oid])) {
1498
            throw new \InvalidArgumentException("Document has no identity.");
1499
        }
1500
1501 227
        if (isset($this->documentDeletions[$oid])) {
1502
            throw new \InvalidArgumentException("Document is removed.");
1503
        }
1504
1505 227
        if ( ! isset($this->documentUpdates[$oid])
1506 227
            && ! isset($this->documentInsertions[$oid])
1507 227
            && ! isset($this->documentUpserts[$oid])) {
1508 223
            $this->documentUpdates[$oid] = $document;
1509 223
        }
1510 227
    }
1511
1512
    /**
1513
     * Checks whether a document is registered as dirty in the unit of work.
1514
     * Note: Is not very useful currently as dirty documents are only registered
1515
     * at commit time.
1516
     *
1517
     * @param object $document
1518
     * @return boolean
1519
     */
1520 13
    public function isScheduledForUpdate($document)
1521
    {
1522 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1523
    }
1524
1525 1
    public function isScheduledForDirtyCheck($document)
1526
    {
1527 1
        $class = $this->dm->getClassMetadata(get_class($document));
1528 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1529
    }
1530
1531
    /**
1532
     * INTERNAL:
1533
     * Schedules a document for deletion.
1534
     *
1535
     * @param object $document
1536
     */
1537 68
    public function scheduleForDelete($document)
1538
    {
1539 68
        $oid = spl_object_hash($document);
1540
1541 68
        if (isset($this->documentInsertions[$oid])) {
1542 2
            if ($this->isInIdentityMap($document)) {
1543 2
                $this->removeFromIdentityMap($document);
1544 2
            }
1545 2
            unset($this->documentInsertions[$oid]);
1546 2
            return; // document has not been persisted yet, so nothing more to do.
1547
        }
1548
1549 67
        if ( ! $this->isInIdentityMap($document)) {
1550 1
            return; // ignore
1551
        }
1552
1553 66
        $this->removeFromIdentityMap($document);
1554 66
        $this->documentStates[$oid] = self::STATE_REMOVED;
1555
1556 66
        if (isset($this->documentUpdates[$oid])) {
1557
            unset($this->documentUpdates[$oid]);
1558
        }
1559 66
        if ( ! isset($this->documentDeletions[$oid])) {
1560 66
            $this->documentDeletions[$oid] = $document;
1561 66
        }
1562 66
    }
1563
1564
    /**
1565
     * Checks whether a document is registered as removed/deleted with the unit
1566
     * of work.
1567
     *
1568
     * @param object $document
1569
     * @return boolean
1570
     */
1571 8
    public function isScheduledForDelete($document)
1572
    {
1573 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1574
    }
1575
1576
    /**
1577
     * Checks whether a document is scheduled for insertion, update or deletion.
1578
     *
1579
     * @param $document
1580
     * @return boolean
1581
     */
1582 226
    public function isDocumentScheduled($document)
1583
    {
1584 226
        $oid = spl_object_hash($document);
1585 226
        return isset($this->documentInsertions[$oid]) ||
1586 121
            isset($this->documentUpserts[$oid]) ||
1587 112
            isset($this->documentUpdates[$oid]) ||
1588 226
            isset($this->documentDeletions[$oid]);
1589
    }
1590
1591
    /**
1592
     * INTERNAL:
1593
     * Registers a document in the identity map.
1594
     *
1595
     * Note that documents in a hierarchy are registered with the class name of
1596
     * the root document. Identifiers are serialized before being used as array
1597
     * keys to allow differentiation of equal, but not identical, values.
1598
     *
1599
     * @ignore
1600
     * @param object $document  The document to register.
1601
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1602
     *                  the document in question is already managed.
1603
     */
1604 606
    public function addToIdentityMap($document)
1605
    {
1606 606
        $class = $this->dm->getClassMetadata(get_class($document));
1607 606
        $id = $this->getIdForIdentityMap($document);
1608
1609 606
        if (isset($this->identityMap[$class->name][$id])) {
1610 54
            return false;
1611
        }
1612
1613 606
        $this->identityMap[$class->name][$id] = $document;
1614
1615 606
        if ($document instanceof NotifyPropertyChanged &&
1616 606
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1617 3
            $document->addPropertyChangedListener($this);
1618 3
        }
1619
1620 606
        return true;
1621
    }
1622
1623
    /**
1624
     * Gets the state of a document with regard to the current unit of work.
1625
     *
1626
     * @param object   $document
1627
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1628
     *                         This parameter can be set to improve performance of document state detection
1629
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1630
     *                         is either known or does not matter for the caller of the method.
1631
     * @return int The document state.
1632
     */
1633 580
    public function getDocumentState($document, $assume = null)
1634
    {
1635 580
        $oid = spl_object_hash($document);
1636
1637 580
        if (isset($this->documentStates[$oid])) {
1638 356
            return $this->documentStates[$oid];
1639
        }
1640
1641 580
        $class = $this->dm->getClassMetadata(get_class($document));
1642
1643 580
        if ($class->isEmbeddedDocument) {
1644 176
            return self::STATE_NEW;
1645
        }
1646
1647 577
        if ($assume !== null) {
1648 574
            return $assume;
1649
        }
1650
1651
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1652
         * known. Note that you cannot remember the NEW or DETACHED state in
1653
         * _documentStates since the UoW does not hold references to such
1654
         * objects and the object hash can be reused. More generally, because
1655
         * the state may "change" between NEW/DETACHED without the UoW being
1656
         * aware of it.
1657
         */
1658 4
        $id = $class->getIdentifierObject($document);
1659
1660 4
        if ($id === null) {
1661 2
            return self::STATE_NEW;
1662
        }
1663
1664
        // Check for a version field, if available, to avoid a DB lookup.
1665 2
        if ($class->isVersioned) {
1666
            return ($class->getFieldValue($document, $class->versionField))
1667
                ? self::STATE_DETACHED
1668
                : self::STATE_NEW;
1669
        }
1670
1671
        // Last try before DB lookup: check the identity map.
1672 2
        if ($this->tryGetById($id, $class)) {
1673 1
            return self::STATE_DETACHED;
1674
        }
1675
1676
        // DB lookup
1677 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1678 1
            return self::STATE_DETACHED;
1679
        }
1680
1681 1
        return self::STATE_NEW;
1682
    }
1683
1684
    /**
1685
     * INTERNAL:
1686
     * Removes a document from the identity map. This effectively detaches the
1687
     * document from the persistence management of Doctrine.
1688
     *
1689
     * @ignore
1690
     * @param object $document
1691
     * @throws \InvalidArgumentException
1692
     * @return boolean
1693
     */
1694 77
    public function removeFromIdentityMap($document)
1695
    {
1696 77
        $oid = spl_object_hash($document);
1697
1698
        // Check if id is registered first
1699 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1700
            return false;
1701
        }
1702
1703 77
        $class = $this->dm->getClassMetadata(get_class($document));
1704 77
        $id = $this->getIdForIdentityMap($document);
1705
1706 77
        if (isset($this->identityMap[$class->name][$id])) {
1707 77
            unset($this->identityMap[$class->name][$id]);
1708 77
            $this->documentStates[$oid] = self::STATE_DETACHED;
1709 77
            return true;
1710
        }
1711
1712
        return false;
1713
    }
1714
1715
    /**
1716
     * INTERNAL:
1717
     * Gets a document in the identity map by its identifier hash.
1718
     *
1719
     * @ignore
1720
     * @param mixed         $id    Document identifier
1721
     * @param ClassMetadata $class Document class
1722
     * @return object
1723
     * @throws InvalidArgumentException if the class does not have an identifier
1724
     */
1725 31
    public function getById($id, ClassMetadata $class)
1726
    {
1727 31
        if ( ! $class->identifier) {
1728
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1729
        }
1730
1731 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1732
1733 31
        return $this->identityMap[$class->name][$serializedId];
1734
    }
1735
1736
    /**
1737
     * INTERNAL:
1738
     * Tries to get a document by its identifier hash. If no document is found
1739
     * for the given hash, FALSE is returned.
1740
     *
1741
     * @ignore
1742
     * @param mixed         $id    Document identifier
1743
     * @param ClassMetadata $class Document class
1744
     * @return mixed The found document or FALSE.
1745
     * @throws InvalidArgumentException if the class does not have an identifier
1746
     */
1747 291
    public function tryGetById($id, ClassMetadata $class)
1748
    {
1749 291
        if ( ! $class->identifier) {
1750
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1751
        }
1752
1753 291
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1754
1755 291
        return isset($this->identityMap[$class->name][$serializedId]) ?
1756 291
            $this->identityMap[$class->name][$serializedId] : false;
1757
    }
1758
1759
    /**
1760
     * Schedules a document for dirty-checking at commit-time.
1761
     *
1762
     * @param object $document The document to schedule for dirty-checking.
1763
     * @todo Rename: scheduleForSynchronization
1764
     */
1765 2
    public function scheduleForDirtyCheck($document)
1766
    {
1767 2
        $class = $this->dm->getClassMetadata(get_class($document));
1768 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1769 2
    }
1770
1771
    /**
1772
     * Checks whether a document is registered in the identity map.
1773
     *
1774
     * @param object $document
1775
     * @return boolean
1776
     */
1777 77
    public function isInIdentityMap($document)
1778
    {
1779 77
        $oid = spl_object_hash($document);
1780
1781 77
        if ( ! isset($this->documentIdentifiers[$oid])) {
1782 4
            return false;
1783
        }
1784
1785 76
        $class = $this->dm->getClassMetadata(get_class($document));
1786 76
        $id = $this->getIdForIdentityMap($document);
1787
1788 76
        return isset($this->identityMap[$class->name][$id]);
1789
    }
1790
1791
    /**
1792
     * @param object $document
1793
     * @return string
1794
     */
1795 606
    private function getIdForIdentityMap($document)
1796
    {
1797 606
        $class = $this->dm->getClassMetadata(get_class($document));
1798
1799 606
        if ( ! $class->identifier) {
1800 146
            $id = spl_object_hash($document);
1801 146
        } else {
1802 605
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1803 605
            $id = serialize($class->getDatabaseIdentifierValue($id));
1804
        }
1805
1806 606
        return $id;
1807
    }
1808
1809
    /**
1810
     * INTERNAL:
1811
     * Checks whether an identifier exists in the identity map.
1812
     *
1813
     * @ignore
1814
     * @param string $id
1815
     * @param string $rootClassName
1816
     * @return boolean
1817
     */
1818
    public function containsId($id, $rootClassName)
1819
    {
1820
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1821
    }
1822
1823
    /**
1824
     * Persists a document as part of the current unit of work.
1825
     *
1826
     * @param object $document The document to persist.
1827
     * @throws MongoDBException If trying to persist MappedSuperclass.
1828
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1829
     */
1830 575
    public function persist($document)
1831
    {
1832 575
        $class = $this->dm->getClassMetadata(get_class($document));
1833 575
        if ($class->isMappedSuperclass || $class->isAggregationResultDocument) {
1834 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1835
        }
1836 574
        $visited = array();
1837 574
        $this->doPersist($document, $visited);
1838 570
    }
1839
1840
    /**
1841
     * Saves a document as part of the current unit of work.
1842
     * This method is internally called during save() cascades as it tracks
1843
     * the already visited documents to prevent infinite recursions.
1844
     *
1845
     * NOTE: This method always considers documents that are not yet known to
1846
     * this UnitOfWork as NEW.
1847
     *
1848
     * @param object $document The document to persist.
1849
     * @param array $visited The already visited documents.
1850
     * @throws \InvalidArgumentException
1851
     * @throws MongoDBException
1852
     */
1853 574
    private function doPersist($document, array &$visited)
1854
    {
1855 574
        $oid = spl_object_hash($document);
1856 574
        if (isset($visited[$oid])) {
1857 24
            return; // Prevent infinite recursion
1858
        }
1859
1860 574
        $visited[$oid] = $document; // Mark visited
1861
1862 574
        $class = $this->dm->getClassMetadata(get_class($document));
1863
1864 574
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1865
        switch ($documentState) {
1866 574
            case self::STATE_MANAGED:
1867
                // Nothing to do, except if policy is "deferred explicit"
1868 45
                if ($class->isChangeTrackingDeferredExplicit()) {
1869
                    $this->scheduleForDirtyCheck($document);
1870
                }
1871 45
                break;
1872 574
            case self::STATE_NEW:
1873 574
                $this->persistNew($class, $document);
1874 572
                break;
1875
1876 2
            case self::STATE_REMOVED:
1877
                // Document becomes managed again
1878 2
                unset($this->documentDeletions[$oid]);
1879
1880 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1881 2
                break;
1882
1883
            case self::STATE_DETACHED:
1884
                throw new \InvalidArgumentException(
1885
                    "Behavior of persist() for a detached document is not yet defined.");
1886
1887
            default:
1888
                throw MongoDBException::invalidDocumentState($documentState);
1889
        }
1890
1891 572
        $this->cascadePersist($document, $visited);
1892 570
    }
1893
1894
    /**
1895
     * Deletes a document as part of the current unit of work.
1896
     *
1897
     * @param object $document The document to remove.
1898
     */
1899 67
    public function remove($document)
1900
    {
1901 67
        $visited = array();
1902 67
        $this->doRemove($document, $visited);
1903 67
    }
1904
1905
    /**
1906
     * Deletes a document as part of the current unit of work.
1907
     *
1908
     * This method is internally called during delete() cascades as it tracks
1909
     * the already visited documents to prevent infinite recursions.
1910
     *
1911
     * @param object $document The document to delete.
1912
     * @param array $visited The map of the already visited documents.
1913
     * @throws MongoDBException
1914
     */
1915 67
    private function doRemove($document, array &$visited)
1916
    {
1917 67
        $oid = spl_object_hash($document);
1918 67
        if (isset($visited[$oid])) {
1919 1
            return; // Prevent infinite recursion
1920
        }
1921
1922 67
        $visited[$oid] = $document; // mark visited
1923
1924
        /* Cascade first, because scheduleForDelete() removes the entity from
1925
         * the identity map, which can cause problems when a lazy Proxy has to
1926
         * be initialized for the cascade operation.
1927
         */
1928 67
        $this->cascadeRemove($document, $visited);
1929
1930 67
        $class = $this->dm->getClassMetadata(get_class($document));
1931 67
        $documentState = $this->getDocumentState($document);
1932
        switch ($documentState) {
1933 67
            case self::STATE_NEW:
1934 67
            case self::STATE_REMOVED:
1935
                // nothing to do
1936 1
                break;
1937 67
            case self::STATE_MANAGED:
1938 67 View Code Duplication
                if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) {
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...
1939 8
                    $class->invokeLifecycleCallbacks(Events::preRemove, $document, array(new LifecycleEventArgs($document, $this->dm)));
1940 8
                }
1941 67 View Code Duplication
                if ($this->evm->hasListeners(Events::preRemove)) {
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...
1942 1
                    $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm));
1943 1
                }
1944 67
                $this->scheduleForDelete($document);
1945 67
                break;
1946
            case self::STATE_DETACHED:
1947
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1948
            default:
1949
                throw MongoDBException::invalidDocumentState($documentState);
1950
        }
1951 67
    }
1952
1953
    /**
1954
     * Merges the state of the given detached document into this UnitOfWork.
1955
     *
1956
     * @param object $document
1957
     * @return object The managed copy of the document.
1958
     */
1959 13
    public function merge($document)
1960
    {
1961 13
        $visited = array();
1962
1963 13
        return $this->doMerge($document, $visited);
1964
    }
1965
1966
    /**
1967
     * Executes a merge operation on a document.
1968
     *
1969
     * @param object      $document
1970
     * @param array       $visited
1971
     * @param object|null $prevManagedCopy
1972
     * @param array|null  $assoc
1973
     *
1974
     * @return object The managed copy of the document.
1975
     *
1976
     * @throws InvalidArgumentException If the entity instance is NEW.
1977
     * @throws LockException If the document uses optimistic locking through a
1978
     *                       version attribute and the version check against the
1979
     *                       managed copy fails.
1980
     */
1981 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1982
    {
1983 13
        $oid = spl_object_hash($document);
1984
1985 13
        if (isset($visited[$oid])) {
1986 1
            return $visited[$oid]; // Prevent infinite recursion
1987
        }
1988
1989 13
        $visited[$oid] = $document; // mark visited
1990
1991 13
        $class = $this->dm->getClassMetadata(get_class($document));
1992
1993
        /* First we assume DETACHED, although it can still be NEW but we can
1994
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1995
         * an identity, we need to fetch it from the DB anyway in order to
1996
         * merge. MANAGED documents are ignored by the merge operation.
1997
         */
1998 13
        $managedCopy = $document;
1999
2000 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2001 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
2002
                $document->__load();
2003
            }
2004
2005
            // Try to look the document up in the identity map.
2006 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
2007
2008 13
            if ($id === null) {
2009
                // If there is no identifier, it is actually NEW.
2010 5
                $managedCopy = $class->newInstance();
2011 5
                $this->persistNew($class, $managedCopy);
2012 5
            } else {
2013 10
                $managedCopy = $this->tryGetById($id, $class);
2014
2015 10
                if ($managedCopy) {
2016
                    // We have the document in memory already, just make sure it is not removed.
2017 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
2018
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
2019
                    }
2020 5
                } else {
2021
                    // We need to fetch the managed copy in order to merge.
2022 7
                    $managedCopy = $this->dm->find($class->name, $id);
2023
                }
2024
2025 10
                if ($managedCopy === null) {
2026
                    // If the identifier is ASSIGNED, it is NEW
2027
                    $managedCopy = $class->newInstance();
2028
                    $class->setIdentifierValue($managedCopy, $id);
2029
                    $this->persistNew($class, $managedCopy);
2030
                } else {
2031 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...
2032
                        $managedCopy->__load();
2033
                    }
2034
                }
2035
            }
2036
2037 13
            if ($class->isVersioned) {
2038
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
2039
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2040
2041
                // Throw exception if versions don't match
2042
                if ($managedCopyVersion != $documentVersion) {
2043
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
2044
                }
2045
            }
2046
2047
            // Merge state of $document into existing (managed) document
2048 13
            foreach ($class->reflClass->getProperties() as $prop) {
2049 13
                $name = $prop->name;
2050 13
                $prop->setAccessible(true);
2051 13
                if ( ! isset($class->associationMappings[$name])) {
2052 13
                    if ( ! $class->isIdentifier($name)) {
2053 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
2054 13
                    }
2055 13
                } else {
2056 13
                    $assoc2 = $class->associationMappings[$name];
2057
2058 13
                    if ($assoc2['type'] === 'one') {
2059 5
                        $other = $prop->getValue($document);
2060
2061 5
                        if ($other === null) {
2062 2
                            $prop->setValue($managedCopy, null);
2063 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...
2064
                            // Do not merge fields marked lazy that have not been fetched
2065 1
                            continue;
2066 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
2067
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
2068
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
2069
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
2070
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
2071
                                $relatedId = $targetClass->getIdentifierObject($other);
2072
2073
                                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...
2074
                                    $other = $this->dm->find($targetClass->name, $relatedId);
2075
                                } else {
2076
                                    $other = $this
2077
                                        ->dm
2078
                                        ->getProxyFactory()
2079
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
2080
                                    $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...
2081
                                }
2082
                            }
2083
2084
                            $prop->setValue($managedCopy, $other);
2085
                        }
2086 4
                    } else {
2087 10
                        $mergeCol = $prop->getValue($document);
2088
2089 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
2090
                            /* Do not merge fields marked lazy that have not
2091
                             * been fetched. Keep the lazy persistent collection
2092
                             * of the managed copy.
2093
                             */
2094 3
                            continue;
2095
                        }
2096
2097 7
                        $managedCol = $prop->getValue($managedCopy);
2098
2099 7
                        if ( ! $managedCol) {
2100 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
2101 2
                            $managedCol->setOwner($managedCopy, $assoc2);
2102 2
                            $prop->setValue($managedCopy, $managedCol);
2103 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
2104 2
                        }
2105
2106
                        /* Note: do not process association's target documents.
2107
                         * They will be handled during the cascade. Initialize
2108
                         * and, if necessary, clear $managedCol for now.
2109
                         */
2110 7
                        if ($assoc2['isCascadeMerge']) {
2111 7
                            $managedCol->initialize();
2112
2113
                            // If $managedCol differs from the merged collection, clear and set dirty
2114 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
2115 2
                                $managedCol->unwrap()->clear();
2116 2
                                $managedCol->setDirty(true);
2117
2118 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
2119
                                    $this->scheduleForDirtyCheck($managedCopy);
2120
                                }
2121 2
                            }
2122 7
                        }
2123
                    }
2124
                }
2125
2126 13
                if ($class->isChangeTrackingNotify()) {
2127
                    // Just treat all properties as changed, there is no other choice.
2128
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2129
                }
2130 13
            }
2131
2132 13
            if ($class->isChangeTrackingDeferredExplicit()) {
2133
                $this->scheduleForDirtyCheck($document);
2134
            }
2135 13
        }
2136
2137 13
        if ($prevManagedCopy !== null) {
2138 6
            $assocField = $assoc['fieldName'];
2139 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
2140
2141 6
            if ($assoc['type'] === 'one') {
2142 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
2143 2
            } else {
2144 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
2145
2146 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
2147 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
2148 1
                }
2149
            }
2150 6
        }
2151
2152
        // Mark the managed copy visited as well
2153 13
        $visited[spl_object_hash($managedCopy)] = true;
2154
2155 13
        $this->cascadeMerge($document, $managedCopy, $visited);
2156
2157 13
        return $managedCopy;
2158
    }
2159
2160
    /**
2161
     * Detaches a document from the persistence management. It's persistence will
2162
     * no longer be managed by Doctrine.
2163
     *
2164
     * @param object $document The document to detach.
2165
     */
2166 9
    public function detach($document)
2167
    {
2168 9
        $visited = array();
2169 9
        $this->doDetach($document, $visited);
2170 9
    }
2171
2172
    /**
2173
     * Executes a detach operation on the given document.
2174
     *
2175
     * @param object $document
2176
     * @param array $visited
2177
     * @internal This method always considers documents with an assigned identifier as DETACHED.
2178
     */
2179 12
    private function doDetach($document, array &$visited)
2180
    {
2181 12
        $oid = spl_object_hash($document);
2182 12
        if (isset($visited[$oid])) {
2183 4
            return; // Prevent infinite recursion
2184
        }
2185
2186 12
        $visited[$oid] = $document; // mark visited
2187
2188 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2189 12
            case self::STATE_MANAGED:
2190 12
                $this->removeFromIdentityMap($document);
2191 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2192 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2193 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2194 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2195 12
                    $this->hasScheduledCollections[$oid]);
2196 12
                break;
2197 4
            case self::STATE_NEW:
2198 4
            case self::STATE_DETACHED:
2199 4
                return;
2200 12
        }
2201
2202 12
        $this->cascadeDetach($document, $visited);
2203 12
    }
2204
2205
    /**
2206
     * Refreshes the state of the given document from the database, overwriting
2207
     * any local, unpersisted changes.
2208
     *
2209
     * @param object $document The document to refresh.
2210
     * @throws \InvalidArgumentException If the document is not MANAGED.
2211
     */
2212 21
    public function refresh($document)
2213
    {
2214 21
        $visited = array();
2215 21
        $this->doRefresh($document, $visited);
2216 20
    }
2217
2218
    /**
2219
     * Executes a refresh operation on a document.
2220
     *
2221
     * @param object $document The document to refresh.
2222
     * @param array $visited The already visited documents during cascades.
2223
     * @throws \InvalidArgumentException If the document is not MANAGED.
2224
     */
2225 21
    private function doRefresh($document, array &$visited)
2226
    {
2227 21
        $oid = spl_object_hash($document);
2228 21
        if (isset($visited[$oid])) {
2229
            return; // Prevent infinite recursion
2230
        }
2231
2232 21
        $visited[$oid] = $document; // mark visited
2233
2234 21
        $class = $this->dm->getClassMetadata(get_class($document));
2235
2236 21
        if ( ! $class->isEmbeddedDocument) {
2237 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2238 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2239 20
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2240 20
            } else {
2241 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2242
            }
2243 20
        }
2244
2245 20
        $this->cascadeRefresh($document, $visited);
2246 20
    }
2247
2248
    /**
2249
     * Cascades a refresh operation to associated documents.
2250
     *
2251
     * @param object $document
2252
     * @param array $visited
2253
     */
2254 20
    private function cascadeRefresh($document, array &$visited)
2255
    {
2256 20
        $class = $this->dm->getClassMetadata(get_class($document));
2257
2258 20
        $associationMappings = array_filter(
2259 20
            $class->associationMappings,
2260
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2261 20
        );
2262
2263 20
        foreach ($associationMappings as $mapping) {
2264 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2265 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2266 15
                if ($relatedDocuments instanceof PersistentCollection) {
2267
                    // Unwrap so that foreach() does not initialize
2268 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2269 15
                }
2270 15
                foreach ($relatedDocuments as $relatedDocument) {
2271
                    $this->doRefresh($relatedDocument, $visited);
2272 15
                }
2273 15
            } elseif ($relatedDocuments !== null) {
2274 2
                $this->doRefresh($relatedDocuments, $visited);
2275 2
            }
2276 20
        }
2277 20
    }
2278
2279
    /**
2280
     * Cascades a detach operation to associated documents.
2281
     *
2282
     * @param object $document
2283
     * @param array $visited
2284
     */
2285 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...
2286
    {
2287 12
        $class = $this->dm->getClassMetadata(get_class($document));
2288 12
        foreach ($class->fieldMappings as $mapping) {
2289 12
            if ( ! $mapping['isCascadeDetach']) {
2290 12
                continue;
2291
            }
2292 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2293 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2294 7
                if ($relatedDocuments instanceof PersistentCollection) {
2295
                    // Unwrap so that foreach() does not initialize
2296 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2297 6
                }
2298 7
                foreach ($relatedDocuments as $relatedDocument) {
2299 5
                    $this->doDetach($relatedDocument, $visited);
2300 7
                }
2301 7
            } elseif ($relatedDocuments !== null) {
2302 5
                $this->doDetach($relatedDocuments, $visited);
2303 5
            }
2304 12
        }
2305 12
    }
2306
    /**
2307
     * Cascades a merge operation to associated documents.
2308
     *
2309
     * @param object $document
2310
     * @param object $managedCopy
2311
     * @param array $visited
2312
     */
2313 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2314
    {
2315 13
        $class = $this->dm->getClassMetadata(get_class($document));
2316
2317 13
        $associationMappings = array_filter(
2318 13
            $class->associationMappings,
2319
            function ($assoc) { return $assoc['isCascadeMerge']; }
2320 13
        );
2321
2322 13
        foreach ($associationMappings as $assoc) {
2323 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2324
2325 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2326 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2327
                    // Collections are the same, so there is nothing to do
2328
                    continue;
2329
                }
2330
2331 8
                if ($relatedDocuments instanceof PersistentCollection) {
2332
                    // Unwrap so that foreach() does not initialize
2333 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2334 6
                }
2335
2336 8
                foreach ($relatedDocuments as $relatedDocument) {
2337 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2338 8
                }
2339 12
            } elseif ($relatedDocuments !== null) {
2340 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2341 3
            }
2342 13
        }
2343 13
    }
2344
2345
    /**
2346
     * Cascades the save operation to associated documents.
2347
     *
2348
     * @param object $document
2349
     * @param array $visited
2350
     */
2351 572
    private function cascadePersist($document, array &$visited)
2352
    {
2353 572
        $class = $this->dm->getClassMetadata(get_class($document));
2354
2355 572
        $associationMappings = array_filter(
2356 572
            $class->associationMappings,
2357
            function ($assoc) { return $assoc['isCascadePersist']; }
2358 572
        );
2359
2360 572
        foreach ($associationMappings as $fieldName => $mapping) {
2361 392
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2362
2363 392
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2364 341
                if ($relatedDocuments instanceof PersistentCollection) {
2365 17
                    if ($relatedDocuments->getOwner() !== $document) {
2366 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2367 2
                    }
2368
                    // Unwrap so that foreach() does not initialize
2369 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2370 17
                }
2371
2372 341
                $count = 0;
2373 341
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2374 188
                    if ( ! empty($mapping['embedded'])) {
2375 113
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2376 113
                        if ($knownParent && $knownParent !== $document) {
2377 4
                            $relatedDocument = clone $relatedDocument;
2378 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2379 4
                        }
2380 113
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2381 113
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2382 113
                    }
2383 188
                    $this->doPersist($relatedDocument, $visited);
2384 340
                }
2385 392
            } elseif ($relatedDocuments !== null) {
2386 120
                if ( ! empty($mapping['embedded'])) {
2387 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2388 66
                    if ($knownParent && $knownParent !== $document) {
2389 5
                        $relatedDocuments = clone $relatedDocuments;
2390 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2391 5
                    }
2392 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2393 66
                }
2394 120
                $this->doPersist($relatedDocuments, $visited);
2395 119
            }
2396 571
        }
2397 570
    }
2398
2399
    /**
2400
     * Cascades the delete operation to associated documents.
2401
     *
2402
     * @param object $document
2403
     * @param array $visited
2404
     */
2405 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...
2406
    {
2407 67
        $class = $this->dm->getClassMetadata(get_class($document));
2408 67
        foreach ($class->fieldMappings as $mapping) {
2409 66
            if ( ! $mapping['isCascadeRemove']) {
2410 66
                continue;
2411
            }
2412 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...
2413 2
                $document->__load();
2414 2
            }
2415
2416 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2417 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2418
                // If its a PersistentCollection initialization is intended! No unwrap!
2419 24
                foreach ($relatedDocuments as $relatedDocument) {
2420 13
                    $this->doRemove($relatedDocument, $visited);
2421 24
                }
2422 33
            } elseif ($relatedDocuments !== null) {
2423 12
                $this->doRemove($relatedDocuments, $visited);
2424 12
            }
2425 67
        }
2426 67
    }
2427
2428
    /**
2429
     * Acquire a lock on the given document.
2430
     *
2431
     * @param object $document
2432
     * @param int $lockMode
2433
     * @param int $lockVersion
2434
     * @throws LockException
2435
     * @throws \InvalidArgumentException
2436
     */
2437 9
    public function lock($document, $lockMode, $lockVersion = null)
2438
    {
2439 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2440 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2441
        }
2442
2443 8
        $documentName = get_class($document);
2444 8
        $class = $this->dm->getClassMetadata($documentName);
2445
2446 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2447 3
            if ( ! $class->isVersioned) {
2448 1
                throw LockException::notVersioned($documentName);
2449
            }
2450
2451 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...
2452 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2453 2
                if ($documentVersion != $lockVersion) {
2454 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2455
                }
2456 1
            }
2457 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2458 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2459 5
        }
2460 6
    }
2461
2462
    /**
2463
     * Releases a lock on the given document.
2464
     *
2465
     * @param object $document
2466
     * @throws \InvalidArgumentException
2467
     */
2468 1
    public function unlock($document)
2469
    {
2470 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2471
            throw new \InvalidArgumentException("Document is not MANAGED.");
2472
        }
2473 1
        $documentName = get_class($document);
2474 1
        $this->getDocumentPersister($documentName)->unlock($document);
2475 1
    }
2476
2477
    /**
2478
     * Clears the UnitOfWork.
2479
     *
2480
     * @param string|null $documentName if given, only documents of this type will get detached.
2481
     */
2482 390
    public function clear($documentName = null)
2483
    {
2484 390
        if ($documentName === null) {
2485 384
            $this->identityMap =
2486 384
            $this->documentIdentifiers =
2487 384
            $this->originalDocumentData =
2488 384
            $this->documentChangeSets =
2489 384
            $this->documentStates =
2490 384
            $this->scheduledForDirtyCheck =
2491 384
            $this->documentInsertions =
2492 384
            $this->documentUpserts =
2493 384
            $this->documentUpdates =
2494 384
            $this->documentDeletions =
2495 384
            $this->collectionUpdates =
2496 384
            $this->collectionDeletions =
2497 384
            $this->parentAssociations =
2498 384
            $this->orphanRemovals = 
2499 384
            $this->hasScheduledCollections = array();
2500 384
        } else {
2501 6
            $visited = array();
2502 6
            foreach ($this->identityMap as $className => $documents) {
2503 6
                if ($className === $documentName) {
2504 3
                    foreach ($documents as $document) {
2505 3
                        $this->doDetach($document, $visited);
2506 3
                    }
2507 3
                }
2508 6
            }
2509
        }
2510
2511 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...
2512
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2513
        }
2514 390
    }
2515
2516
    /**
2517
     * INTERNAL:
2518
     * Schedules an embedded document for removal. The remove() operation will be
2519
     * invoked on that document at the beginning of the next commit of this
2520
     * UnitOfWork.
2521
     *
2522
     * @ignore
2523
     * @param object $document
2524
     */
2525 48
    public function scheduleOrphanRemoval($document)
2526
    {
2527 48
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2528 48
    }
2529
2530
    /**
2531
     * INTERNAL:
2532
     * Unschedules an embedded or referenced object for removal.
2533
     *
2534
     * @ignore
2535
     * @param object $document
2536
     */
2537 103
    public function unscheduleOrphanRemoval($document)
2538
    {
2539 103
        $oid = spl_object_hash($document);
2540 103
        if (isset($this->orphanRemovals[$oid])) {
2541 1
            unset($this->orphanRemovals[$oid]);
2542 1
        }
2543 103
    }
2544
2545
    /**
2546
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2547
     *  1) sets owner if it was cloned
2548
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2549
     *  3) NOP if state is OK
2550
     * Returned collection should be used from now on (only important with 2nd point)
2551
     *
2552
     * @param PersistentCollection $coll
2553
     * @param object $document
2554
     * @param ClassMetadata $class
2555
     * @param string $propName
2556
     * @return PersistentCollection
2557
     */
2558 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2559
    {
2560 8
        $owner = $coll->getOwner();
2561 8
        if ($owner === null) { // cloned
2562 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2563 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2564 2
            if ( ! $coll->isInitialized()) {
2565 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2566 1
            }
2567 2
            $newValue = clone $coll;
2568 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2569 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2570 2
            if ($this->isScheduledForUpdate($document)) {
2571
                // @todo following line should be superfluous once collections are stored in change sets
2572
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2573
            }
2574 2
            return $newValue;
2575
        }
2576 6
        return $coll;
2577
    }
2578
2579
    /**
2580
     * INTERNAL:
2581
     * Schedules a complete collection for removal when this UnitOfWork commits.
2582
     *
2583
     * @param PersistentCollection $coll
2584
     */
2585 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2586
    {
2587 41
        $oid = spl_object_hash($coll);
2588 41
        unset($this->collectionUpdates[$oid]);
2589 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2590 41
            $this->collectionDeletions[$oid] = $coll;
2591 41
            $this->scheduleCollectionOwner($coll);
2592 41
        }
2593 41
    }
2594
2595
    /**
2596
     * Checks whether a PersistentCollection is scheduled for deletion.
2597
     *
2598
     * @param PersistentCollection $coll
2599
     * @return boolean
2600
     */
2601 106
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2602
    {
2603 106
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2604
    }
2605
    
2606
    /**
2607
     * INTERNAL:
2608
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2609
     * 
2610
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2611
     */
2612 207 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...
2613
    {
2614 207
        $oid = spl_object_hash($coll);
2615 207
        if (isset($this->collectionDeletions[$oid])) {
2616 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2617 11
            unset($this->collectionDeletions[$oid]);
2618 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2619 11
        }
2620 207
    }
2621
2622
    /**
2623
     * INTERNAL:
2624
     * Schedules a collection for update when this UnitOfWork commits.
2625
     *
2626
     * @param PersistentCollection $coll
2627
     */
2628 223
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2629
    {
2630 223
        $mapping = $coll->getMapping();
2631 223
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2632
            /* There is no need to $unset collection if it will be $set later
2633
             * This is NOP if collection is not scheduled for deletion
2634
             */
2635 40
            $this->unscheduleCollectionDeletion($coll);
2636 40
        }
2637 223
        $oid = spl_object_hash($coll);
2638 223
        if ( ! isset($this->collectionUpdates[$oid])) {
2639 223
            $this->collectionUpdates[$oid] = $coll;
2640 223
            $this->scheduleCollectionOwner($coll);
2641 223
        }
2642 223
    }
2643
    
2644
    /**
2645
     * INTERNAL:
2646
     * Unschedules a collection from being updated when this UnitOfWork commits.
2647
     * 
2648
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2649
     */
2650 207 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...
2651
    {
2652 207
        $oid = spl_object_hash($coll);
2653 207
        if (isset($this->collectionUpdates[$oid])) {
2654 197
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2655 197
            unset($this->collectionUpdates[$oid]);
2656 197
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2657 197
        }
2658 207
    }
2659
    
2660
    /**
2661
     * Checks whether a PersistentCollection is scheduled for update.
2662
     *
2663
     * @param PersistentCollection $coll
2664
     * @return boolean
2665
     */
2666 122
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2667
    {
2668 122
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2669
    }
2670
2671
    /**
2672
     * INTERNAL:
2673
     * Gets PersistentCollections that have been visited during computing change
2674
     * set of $document
2675
     *
2676
     * @param object $document
2677
     * @return PersistentCollection[]
2678
     */
2679 540
    public function getVisitedCollections($document)
2680
    {
2681 540
        $oid = spl_object_hash($document);
2682 540
        return isset($this->visitedCollections[$oid])
2683 540
                ? $this->visitedCollections[$oid]
2684 540
                : array();
2685
    }
2686
    
2687
    /**
2688
     * INTERNAL:
2689
     * Gets PersistentCollections that are scheduled to update and related to $document
2690
     * 
2691
     * @param object $document
2692
     * @return array
2693
     */
2694 540
    public function getScheduledCollections($document)
2695
    {
2696 540
        $oid = spl_object_hash($document);
2697 540
        return isset($this->hasScheduledCollections[$oid]) 
2698 540
                ? $this->hasScheduledCollections[$oid]
2699 540
                : array();
2700
    }
2701
    
2702
    /**
2703
     * Checks whether the document is related to a PersistentCollection
2704
     * scheduled for update or deletion.
2705
     *
2706
     * @param object $document
2707
     * @return boolean
2708
     */
2709 60
    public function hasScheduledCollections($document)
2710
    {
2711 60
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2712
    }
2713
    
2714
    /**
2715
     * Marks the PersistentCollection's top-level owner as having a relation to
2716
     * a collection scheduled for update or deletion.
2717
     *
2718
     * If the owner is not scheduled for any lifecycle action, it will be
2719
     * scheduled for update to ensure that versioning takes place if necessary.
2720
     *
2721
     * If the collection is nested within atomic collection, it is immediately
2722
     * unscheduled and atomic one is scheduled for update instead. This makes
2723
     * calculating update data way easier.
2724
     * 
2725
     * @param PersistentCollection $coll
2726
     */
2727 225
    private function scheduleCollectionOwner(PersistentCollection $coll)
2728
    {
2729 225
        $document = $this->getOwningDocument($coll->getOwner());
2730 225
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2731
2732 225
        if ($document !== $coll->getOwner()) {
2733 24
            $parent = $coll->getOwner();
2734 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2735 24
                list($mapping, $parent, ) = $parentAssoc;
2736 24
            }
2737 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2738 7
                $class = $this->dm->getClassMetadata(get_class($document));
2739 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2740 7
                $this->scheduleCollectionUpdate($atomicCollection);
2741 7
                $this->unscheduleCollectionDeletion($coll);
2742 7
                $this->unscheduleCollectionUpdate($coll);
2743 7
            }
2744 24
        }
2745
2746 225
        if ( ! $this->isDocumentScheduled($document)) {
2747 92
            $this->scheduleForUpdate($document);
2748 92
        }
2749 225
    }
2750
2751
    /**
2752
     * Get the top-most owning document of a given document
2753
     *
2754
     * If a top-level document is provided, that same document will be returned.
2755
     * For an embedded document, we will walk through parent associations until
2756
     * we find a top-level document.
2757
     *
2758
     * @param object $document
2759
     * @throws \UnexpectedValueException when a top-level document could not be found
2760
     * @return object
2761
     */
2762 227
    public function getOwningDocument($document)
2763
    {
2764 227
        $class = $this->dm->getClassMetadata(get_class($document));
2765 227
        while ($class->isEmbeddedDocument) {
2766 38
            $parentAssociation = $this->getParentAssociation($document);
2767
2768 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...
2769
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2770
            }
2771
2772 38
            list(, $document, ) = $parentAssociation;
2773 38
            $class = $this->dm->getClassMetadata(get_class($document));
2774 38
        }
2775
2776 227
        return $document;
2777
    }
2778
2779
    /**
2780
     * Gets the class name for an association (embed or reference) with respect
2781
     * to any discriminator value.
2782
     *
2783
     * @param array      $mapping Field mapping for the association
2784
     * @param array|null $data    Data for the embedded document or reference
2785
     */
2786 208
    public function getClassNameForAssociation(array $mapping, $data)
2787
    {
2788 208
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2789
2790 208
        $discriminatorValue = null;
2791 208
        if (isset($discriminatorField, $data[$discriminatorField])) {
2792 21
            $discriminatorValue = $data[$discriminatorField];
2793 208
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2794
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2795
        }
2796
2797 208
        if ($discriminatorValue !== null) {
2798 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2799 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2800 21
                : $discriminatorValue;
2801
        }
2802
2803 188
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2804
2805 188 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...
2806 15
            $discriminatorValue = $data[$class->discriminatorField];
2807 188
        } elseif ($class->defaultDiscriminatorValue !== null) {
2808 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2809 1
        }
2810
2811 188
        if ($discriminatorValue !== null) {
2812 16
            return isset($class->discriminatorMap[$discriminatorValue])
2813 16
                ? $class->discriminatorMap[$discriminatorValue]
2814 16
                : $discriminatorValue;
2815
        }
2816
2817 172
        return $mapping['targetDocument'];
2818
    }
2819
2820
    /**
2821
     * INTERNAL:
2822
     * Creates a document. Used for reconstitution of documents during hydration.
2823
     *
2824
     * @ignore
2825
     * @param string $className The name of the document class.
2826
     * @param array $data The data for the document.
2827
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2828
     * @param object The document to be hydrated into in case of creation
2829
     * @return object The document instance.
2830
     * @internal Highly performance-sensitive method.
2831
     */
2832 385
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2833
    {
2834 385
        $class = $this->dm->getClassMetadata($className);
2835
2836
        // @TODO figure out how to remove this
2837 385
        $discriminatorValue = null;
2838 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...
2839 19
            $discriminatorValue = $data[$class->discriminatorField];
2840 385
        } elseif (isset($class->defaultDiscriminatorValue)) {
2841 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2842 2
        }
2843
2844 385
        if ($discriminatorValue !== null) {
2845 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2846 20
                ? $class->discriminatorMap[$discriminatorValue]
2847 20
                : $discriminatorValue;
2848
2849 20
            $class = $this->dm->getClassMetadata($className);
2850
2851 20
            unset($data[$class->discriminatorField]);
2852 20
        }
2853
2854 385
        $isManagedObject = false;
2855 385
        if (! $class->isAggregationResultDocument) {
2856 385
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2857 385
            $serializedId = serialize($id);
2858 385
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2859 385
        }
2860
2861 385
        if ($isManagedObject) {
2862 91
            $document = $this->identityMap[$class->name][$serializedId];
0 ignored issues
show
Bug introduced by
The variable $serializedId does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2863 91
            $oid = spl_object_hash($document);
2864 91
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2865 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...
2866 10
                $overrideLocalValues = true;
2867 10
                if ($document instanceof NotifyPropertyChanged) {
2868
                    $document->addPropertyChangedListener($this);
2869
                }
2870 10
            } else {
2871 87
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2872
            }
2873 91
            if ($overrideLocalValues) {
2874 47
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2875 47
                $this->originalDocumentData[$oid] = $data;
2876 47
            }
2877 91
        } else {
2878 357
            if ($document === null) {
2879 357
                $document = $class->newInstance();
2880 357
            }
2881
2882 357
            if (! $class->isAggregationResultDocument) {
2883 356
                $this->registerManaged($document, $id, $data);
0 ignored issues
show
Bug introduced by
The variable $id does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2884 356
                $oid = spl_object_hash($document);
2885 356
                $this->documentStates[$oid] = self::STATE_MANAGED;
2886 356
                $this->identityMap[$class->name][$serializedId] = $document;
2887 356
            }
2888
2889 357
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2890
2891 357
            if (! $class->isAggregationResultDocument) {
2892 356
                $this->originalDocumentData[$oid] = $data;
0 ignored issues
show
Bug introduced by
The variable $oid does not seem to be defined for all execution paths leading up to this point.

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

Let’s take a look at an example:

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

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

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

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

Available Fixes

  1. Check for existence of the variable explicitly:

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

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

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2893 356
            }
2894
        }
2895
2896 385
        return $document;
2897
    }
2898
2899
    /**
2900
     * Initializes (loads) an uninitialized persistent collection of a document.
2901
     *
2902
     * @param PersistentCollection $collection The collection to initialize.
2903
     */
2904 157
    public function loadCollection(PersistentCollection $collection)
2905
    {
2906 157
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2907 157
    }
2908
2909
    /**
2910
     * Gets the identity map of the UnitOfWork.
2911
     *
2912
     * @return array
2913
     */
2914
    public function getIdentityMap()
2915
    {
2916
        return $this->identityMap;
2917
    }
2918
2919
    /**
2920
     * Gets the original data of a document. The original data is the data that was
2921
     * present at the time the document was reconstituted from the database.
2922
     *
2923
     * @param object $document
2924
     * @return array
2925
     */
2926 1
    public function getOriginalDocumentData($document)
2927
    {
2928 1
        $oid = spl_object_hash($document);
2929 1
        if (isset($this->originalDocumentData[$oid])) {
2930 1
            return $this->originalDocumentData[$oid];
2931
        }
2932
        return array();
2933
    }
2934
2935
    /**
2936
     * @ignore
2937
     */
2938 51
    public function setOriginalDocumentData($document, array $data)
2939
    {
2940 51
        $oid = spl_object_hash($document);
2941 51
        $this->originalDocumentData[$oid] = $data;
2942 51
        unset($this->documentChangeSets[$oid]);
2943 51
    }
2944
2945
    /**
2946
     * INTERNAL:
2947
     * Sets a property value of the original data array of a document.
2948
     *
2949
     * @ignore
2950
     * @param string $oid
2951
     * @param string $property
2952
     * @param mixed $value
2953
     */
2954 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2955
    {
2956 3
        $this->originalDocumentData[$oid][$property] = $value;
2957 3
    }
2958
2959
    /**
2960
     * Gets the identifier of a document.
2961
     *
2962
     * @param object $document
2963
     * @return mixed The identifier value
2964
     */
2965 358
    public function getDocumentIdentifier($document)
2966
    {
2967 358
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2968 358
            $this->documentIdentifiers[spl_object_hash($document)] : null;
2969
    }
2970
2971
    /**
2972
     * Checks whether the UnitOfWork has any pending insertions.
2973
     *
2974
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2975
     */
2976
    public function hasPendingInsertions()
2977
    {
2978
        return ! empty($this->documentInsertions);
2979
    }
2980
2981
    /**
2982
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2983
     * number of documents in the identity map.
2984
     *
2985
     * @return integer
2986
     */
2987 2
    public function size()
2988
    {
2989 2
        $count = 0;
2990 2
        foreach ($this->identityMap as $documentSet) {
2991 2
            $count += count($documentSet);
2992 2
        }
2993 2
        return $count;
2994
    }
2995
2996
    /**
2997
     * INTERNAL:
2998
     * Registers a document as managed.
2999
     *
3000
     * TODO: This method assumes that $id is a valid PHP identifier for the
3001
     * document class. If the class expects its database identifier to be a
3002
     * MongoId, and an incompatible $id is registered (e.g. an integer), the
3003
     * document identifiers map will become inconsistent with the identity map.
3004
     * In the future, we may want to round-trip $id through a PHP and database
3005
     * conversion and throw an exception if it's inconsistent.
3006
     *
3007
     * @param object $document The document.
3008
     * @param array $id The identifier values.
3009
     * @param array $data The original document data.
3010
     */
3011 379
    public function registerManaged($document, $id, array $data)
3012
    {
3013 379
        $oid = spl_object_hash($document);
3014 379
        $class = $this->dm->getClassMetadata(get_class($document));
3015
3016 379
        if ( ! $class->identifier || $id === null) {
3017 102
            $this->documentIdentifiers[$oid] = $oid;
3018 102
        } else {
3019 373
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
3020
        }
3021
3022 379
        $this->documentStates[$oid] = self::STATE_MANAGED;
3023 379
        $this->originalDocumentData[$oid] = $data;
3024 379
        $this->addToIdentityMap($document);
3025 379
    }
3026
3027
    /**
3028
     * INTERNAL:
3029
     * Clears the property changeset of the document with the given OID.
3030
     *
3031
     * @param string $oid The document's OID.
3032
     */
3033 1
    public function clearDocumentChangeSet($oid)
3034
    {
3035 1
        $this->documentChangeSets[$oid] = array();
3036 1
    }
3037
3038
    /* PropertyChangedListener implementation */
3039
3040
    /**
3041
     * Notifies this UnitOfWork of a property change in a document.
3042
     *
3043
     * @param object $document The document that owns the property.
3044
     * @param string $propertyName The name of the property that changed.
3045
     * @param mixed $oldValue The old value of the property.
3046
     * @param mixed $newValue The new value of the property.
3047
     */
3048 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
3049
    {
3050 2
        $oid = spl_object_hash($document);
3051 2
        $class = $this->dm->getClassMetadata(get_class($document));
3052
3053 2
        if ( ! isset($class->fieldMappings[$propertyName])) {
3054 1
            return; // ignore non-persistent fields
3055
        }
3056
3057
        // Update changeset and mark document for synchronization
3058 2
        $this->documentChangeSets[$oid][$propertyName] = array($oldValue, $newValue);
3059 2
        if ( ! isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
3060 2
            $this->scheduleForDirtyCheck($document);
3061 2
        }
3062 2
    }
3063
3064
    /**
3065
     * Gets the currently scheduled document insertions in this UnitOfWork.
3066
     *
3067
     * @return array
3068
     */
3069 5
    public function getScheduledDocumentInsertions()
3070
    {
3071 5
        return $this->documentInsertions;
3072
    }
3073
3074
    /**
3075
     * Gets the currently scheduled document upserts in this UnitOfWork.
3076
     *
3077
     * @return array
3078
     */
3079 3
    public function getScheduledDocumentUpserts()
3080
    {
3081 3
        return $this->documentUpserts;
3082
    }
3083
3084
    /**
3085
     * Gets the currently scheduled document updates in this UnitOfWork.
3086
     *
3087
     * @return array
3088
     */
3089 3
    public function getScheduledDocumentUpdates()
3090
    {
3091 3
        return $this->documentUpdates;
3092
    }
3093
3094
    /**
3095
     * Gets the currently scheduled document deletions in this UnitOfWork.
3096
     *
3097
     * @return array
3098
     */
3099
    public function getScheduledDocumentDeletions()
3100
    {
3101
        return $this->documentDeletions;
3102
    }
3103
3104
    /**
3105
     * Get the currently scheduled complete collection deletions
3106
     *
3107
     * @return array
3108
     */
3109
    public function getScheduledCollectionDeletions()
3110
    {
3111
        return $this->collectionDeletions;
3112
    }
3113
3114
    /**
3115
     * Gets the currently scheduled collection inserts, updates and deletes.
3116
     *
3117
     * @return array
3118
     */
3119
    public function getScheduledCollectionUpdates()
3120
    {
3121
        return $this->collectionUpdates;
3122
    }
3123
3124
    /**
3125
     * Helper method to initialize a lazy loading proxy or persistent collection.
3126
     *
3127
     * @param object
3128
     * @return void
3129
     */
3130
    public function initializeObject($obj)
3131
    {
3132
        if ($obj instanceof Proxy) {
3133
            $obj->__load();
3134
        } elseif ($obj instanceof PersistentCollection) {
3135
            $obj->initialize();
3136
        }
3137
    }
3138
3139 1
    private function objToStr($obj)
3140
    {
3141 1
        return method_exists($obj, '__toString') ? (string)$obj : get_class($obj) . '@' . spl_object_hash($obj);
3142
    }
3143
}
3144