Completed
Pull Request — 1.0.x (#1431)
by Andreas
05:30
created

UnitOfWork::cascadeRefresh()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7.0145

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
ccs 14
cts 15
cp 0.9333
rs 6.7272
cc 7
eloc 14
nc 5
nop 2
crap 7.0145
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
     * @var array
248
     */
249
    private $parentAssociations = array();
250
251
    /**
252
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
253
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
254
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
255
     *
256
     * @var array
257
     */
258
    private $embeddedDocumentsRegistry = array();
259 941
260
    /**
261 941
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
262 941
     *
263 941
     * @param DocumentManager $dm
264 941
     * @param EventManager $evm
265
     * @param HydratorFactory $hydratorFactory
266
     */
267
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269
        $this->dm = $dm;
270
        $this->evm = $evm;
271
        $this->hydratorFactory = $hydratorFactory;
272 678
    }
273
274 678
    /**
275 678
     * Factory for returning new PersistenceBuilder instances used for preparing data into
276 678
     * queries for insert persistence.
277 678
     *
278
     * @return PersistenceBuilder $pb
279
     */
280
    public function getPersistenceBuilder()
281
    {
282
        if ( ! $this->persistenceBuilder) {
283
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
284
        }
285
        return $this->persistenceBuilder;
286
    }
287
288 181
    /**
289
     * Sets the parent association for a given embedded document.
290 181
     *
291 181
     * @param object $document
292 181
     * @param array $mapping
293
     * @param object $parent
294
     * @param string $propertyPath
295
     */
296
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
297
    {
298
        $oid = spl_object_hash($document);
299
        $this->embeddedDocumentsRegistry[$oid] = $document;
300
        $this->parentAssociations[$oid] = array($mapping, $parent, $propertyPath);
301
    }
302
303
    /**
304 209
     * Gets the parent association for a given embedded document.
305
     *
306 209
     *     <code>
307 209
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
308 205
     *     </code>
309
     *
310 165
     * @param object $document
311
     * @return array $association
312
     */
313
    public function getParentAssociation($document)
314
    {
315
        $oid = spl_object_hash($document);
316
        if ( ! isset($this->parentAssociations[$oid])) {
317
            return null;
318
        }
319 676
        return $this->parentAssociations[$oid];
320
    }
321 676
322 662
    /**
323 662
     * Get the document persister instance for the given document name
324 662
     *
325 662
     * @param string $documentName
326 676
     * @return Persisters\DocumentPersister
327
     */
328
    public function getDocumentPersister($documentName)
329
    {
330
        if ( ! isset($this->persisters[$documentName])) {
331
            $class = $this->dm->getClassMetadata($documentName);
332
            $pb = $this->getPersistenceBuilder();
333
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
334 676
        }
335
        return $this->persisters[$documentName];
336 676
    }
337 676
338 676
    /**
339 676
     * Get the collection persister instance.
340 676
     *
341
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
342
     */
343
    public function getCollectionPersister()
344
    {
345
        if ( ! isset($this->collectionPersister)) {
346
            $pb = $this->getPersistenceBuilder();
347
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
348
        }
349 14
        return $this->collectionPersister;
350
    }
351 14
352 14
    /**
353
     * Set the document persister instance to use for the given document name
354
     *
355
     * @param string $documentName
356
     * @param Persisters\DocumentPersister $persister
357
     */
358
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
359
    {
360
        $this->persisters[$documentName] = $persister;
361
    }
362
363
    /**
364
     * Commits the UnitOfWork, executing all operations that have been postponed
365
     * up to this point. The state of all managed documents will be synchronized with
366
     * the database.
367
     *
368 562
     * The operations are executed in the following order:
369
     *
370
     * 1) All document insertions
371 562
     * 2) All document updates
372
     * 3) All document deletions
373
     *
374
     * @param object $document
375 562
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
376 562
     */
377
    public function commit($document = null, array $options = array())
378
    {
379 562
        // Raise preFlush
380
        if ($this->evm->hasListeners(Events::preFlush)) {
381
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
382 562
        }
383 556
384 561
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
385 12
        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...
386 12
            $options = array_merge($defaultOptions, $options);
387 1
        } else {
388 1
            $options = $defaultOptions;
389 1
        }
390 1
        // Compute changes done since last commit.
391
        if ($document === null) {
392 560
            $this->computeChangeSets();
393 241
        } elseif (is_object($document)) {
394 204
            $this->computeSingleDocumentChangeSet($document);
395 194
        } elseif (is_array($document)) {
396 24
            foreach ($document as $object) {
397 24
                $this->computeSingleDocumentChangeSet($object);
398 24
            }
399 560
        }
400 24
401
        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...
402
            $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...
403 557
            $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...
404 46
            $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...
405 46
            $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...
406 46
            $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...
407 46
            $this->orphanRemovals)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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