Completed
Push — 1.0.x ( 55bff8...8c7398 )
by
unknown
09:45
created

UnitOfWork::getIdForIdentityMap()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
558 12
            && ! isset($this->documentUpserts[$oid])
559 12
            && ! isset($this->documentDeletions[$oid])
560 12
            && isset($this->documentStates[$oid])
561 12
        ) {
562 8
            $this->computeChangeSet($class, $document);
563 8
        }
564 12
    }
565
566
    /**
567
     * Gets the changeset for a document.
568
     *
569
     * @param object $document
570
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
571
     */
572 542
    public function getDocumentChangeSet($document)
573
    {
574 542
        $oid = spl_object_hash($document);
575 542
        if (isset($this->documentChangeSets[$oid])) {
576 542
            return $this->documentChangeSets[$oid];
577
        }
578 73
        return array();
579
    }
580
581
    /**
582
     * Get a documents actual data, flattening all the objects to arrays.
583
     *
584
     * @param object $document
585
     * @return array
586
     */
587 556
    public function getDocumentActualData($document)
588
    {
589 556
        $class = $this->dm->getClassMetadata(get_class($document));
590 556
        $actualData = array();
591 556
        foreach ($class->reflFields as $name => $refProp) {
592 556
            $mapping = $class->fieldMappings[$name];
593
            // skip not saved fields
594 556
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
595 49
                continue;
596
            }
597 556
            $value = $refProp->getValue($document);
598 556
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
599 5
                $value = new GridFSFile($value);
600 5
                $class->reflFields[$name]->setValue($document, $value);
601 5
                $actualData[$name] = $value;
602 556
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
603 556
                && $value !== null && ! ($value instanceof PersistentCollection)) {
604
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
605 366
                if ( ! $value instanceof Collection) {
606 119
                    $value = new ArrayCollection($value);
607 119
                }
608
609
                // Inject PersistentCollection
610 366
                $coll = new PersistentCollection($value, $this->dm, $this);
611 366
                $coll->setOwner($document, $mapping);
612 366
                $coll->setDirty( ! $value->isEmpty());
613 366
                $class->reflFields[$name]->setValue($document, $coll);
614 366
                $actualData[$name] = $coll;
615 366
            } else {
616 556
                $actualData[$name] = $value;
617
            }
618 556
        }
619 556
        return $actualData;
620
    }
621
622
    /**
623
     * Computes the changes that happened to a single document.
624
     *
625
     * Modifies/populates the following properties:
626
     *
627
     * {@link originalDocumentData}
628
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
629
     * then it was not fetched from the database and therefore we have no original
630
     * document data yet. All of the current document data is stored as the original document data.
631
     *
632
     * {@link documentChangeSets}
633
     * The changes detected on all properties of the document are stored there.
634
     * A change is a tuple array where the first entry is the old value and the second
635
     * entry is the new value of the property. Changesets are used by persisters
636
     * to INSERT/UPDATE the persistent document state.
637
     *
638
     * {@link documentUpdates}
639
     * If the document is already fully MANAGED (has been fetched from the database before)
640
     * and any changes to its properties are detected, then a reference to the document is stored
641
     * there to mark it for an update.
642
     *
643
     * @param ClassMetadata $class The class descriptor of the document.
644
     * @param object $document The document for which to compute the changes.
645
     */
646 556
    public function computeChangeSet(ClassMetadata $class, $document)
647
    {
648 556
        if ( ! $class->isInheritanceTypeNone()) {
649 172
            $class = $this->dm->getClassMetadata(get_class($document));
650 172
        }
651
652
        // Fire PreFlush lifecycle callbacks
653 556
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
654 10
            $class->invokeLifecycleCallbacks(Events::preFlush, $document);
655 10
        }
656
657 556
        $this->computeOrRecomputeChangeSet($class, $document);
658 555
    }
659
660
    /**
661
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
662
     *
663
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
664
     * @param object $document
665
     * @param boolean $recompute
666
     */
667 556
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
668
    {
669 556
        $oid = spl_object_hash($document);
670 556
        $actualData = $this->getDocumentActualData($document);
671 556
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
672 556
        if ($isNewDocument) {
673
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
674
            // These result in an INSERT.
675 556
            $this->originalDocumentData[$oid] = $actualData;
676 556
            $changeSet = array();
677 556
            foreach ($actualData as $propName => $actualValue) {
678
                /* At this PersistentCollection shouldn't be here, probably it
679
                 * was cloned and its ownership must be fixed
680
                 */
681 556
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
682
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
683
                    $actualValue = $actualData[$propName];
684
                }
685 556
                $changeSet[$propName] = array(null, $actualValue);
686 556
            }
687 556
            $this->documentChangeSets[$oid] = $changeSet;
688 556
        } else {
689
            // Document is "fully" MANAGED: it was already fully persisted before
690
            // and we have a copy of the original data
691 276
            $originalData = $this->originalDocumentData[$oid];
692 276
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
693 276
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
694 2
                $changeSet = $this->documentChangeSets[$oid];
695 2
            } else {
696 276
                $changeSet = array();
697
            }
698
699 276
            foreach ($actualData as $propName => $actualValue) {
700
                // skip not saved fields
701 276
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
702
                    continue;
703
                }
704
705 276
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
706
707
                // skip if value has not changed
708 276
                if ($orgValue === $actualValue) {
709
                    // but consider dirty GridFSFile instances as changed
710 275
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
711 275
                        continue;
712
                    }
713 1
                }
714
715
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
716 177
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
717 10
                    if ($orgValue !== null) {
718 5
                        $this->scheduleOrphanRemoval($orgValue);
719 5
                    }
720
721 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
722 10
                    continue;
723
                }
724
725
                // if owning side of reference-one relationship
726 170
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
727 10
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
728 1
                        $this->scheduleOrphanRemoval($orgValue);
729 1
                    }
730
731 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
732 10
                    continue;
733
                }
734
735 162
                if ($isChangeTrackingNotify) {
736 2
                    continue;
737
                }
738
739
                // ignore inverse side of reference-many relationship
740 161
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'many' && $class->fieldMappings[$propName]['isInverseSide']) {
741
                    continue;
742
                }
743
744
                // Persistent collection was exchanged with the "originally"
745
                // created one. This can only mean it was cloned and replaced
746
                // on another document.
747 161
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
748 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
749 6
                }
750
751
                // if embed-many or reference-many relationship
752 161
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
753 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
754
                    /* If original collection was exchanged with a non-empty value
755
                     * and $set will be issued, there is no need to $unset it first
756
                     */
757 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
758 7
                        continue;
759
                    }
760 19
                    if ($orgValue instanceof PersistentCollection) {
761 17
                        $this->scheduleCollectionDeletion($orgValue);
762 17
                    }
763 19
                    continue;
764
                }
765
766
                // skip equivalent date values
767 147
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
768 35
                    $dateType = Type::getType('date');
769 35
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
770 35
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
771
772 35
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
773 29
                        continue;
774
                    }
775 9
                }
776
777
                // regular field
778 131
                $changeSet[$propName] = array($orgValue, $actualValue);
779 276
            }
780 276
            if ($changeSet) {
781 163
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
782 163
                    ? $changeSet + $this->documentChangeSets[$oid]
783 15
                    : $changeSet;
784
785 163
                $this->originalDocumentData[$oid] = $actualData;
786 163
                $this->scheduleForUpdate($document);
787 163
            }
788
        }
789
790
        // Look for changes in associations of the document
791 556
        $associationMappings = array_filter(
792 556
            $class->associationMappings,
793
            function ($assoc) { return empty($assoc['notSaved']); }
794 556
        );
795
796 556
        foreach ($associationMappings as $mapping) {
797 428
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
798
799 428
            if ($value === null) {
800 288
                continue;
801
            }
802
803 419
            $this->computeAssociationChanges($document, $mapping, $value);
804
805 418
            if (isset($mapping['reference'])) {
806 315
                continue;
807
            }
808
809 327
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
810
811 327
            foreach ($values as $obj) {
812 169
                $oid2 = spl_object_hash($obj);
813
814 169
                if (isset($this->documentChangeSets[$oid2])) {
815 167
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
816
817 167
                    if ( ! $isNewDocument) {
818 71
                        $this->scheduleForUpdate($document);
819 71
                    }
820
821 167
                    break;
822
                }
823 327
            }
824 555
        }
825 555
    }
826
827
    /**
828
     * Computes all the changes that have been done to documents and collections
829
     * since the last commit and stores these changes in the _documentChangeSet map
830
     * temporarily for access by the persisters, until the UoW commit is finished.
831
     */
832 554
    public function computeChangeSets()
833
    {
834 554
        $this->computeScheduleInsertsChangeSets();
835 553
        $this->computeScheduleUpsertsChangeSets();
836
837
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
838 553
        foreach ($this->identityMap as $className => $documents) {
839 553
            $class = $this->dm->getClassMetadata($className);
840 553
            if ($class->isEmbeddedDocument) {
841
                /* we do not want to compute changes to embedded documents up front
842
                 * in case embedded document was replaced and its changeset
843
                 * would corrupt data. Embedded documents' change set will
844
                 * be calculated by reachability from owning document.
845
                 */
846 158
                continue;
847
            }
848
849
            // If change tracking is explicit or happens through notification, then only compute
850
            // changes on document of that type that are explicitly marked for synchronization.
851 553
            switch (true) {
852 553
                case ($class->isChangeTrackingDeferredImplicit()):
853 552
                    $documentsToProcess = $documents;
854 552
                    break;
855
856 3
                case (isset($this->scheduledForDirtyCheck[$className])):
857 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
858 2
                    break;
859
860 3
                default:
861 3
                    $documentsToProcess = array();
862
863 3
            }
864
865 553
            foreach ($documentsToProcess as $document) {
866
                // Ignore uninitialized proxy objects
867 549
                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...
868 10
                    continue;
869
                }
870
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
871 549
                $oid = spl_object_hash($document);
872 549 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...
873 549
                    && ! isset($this->documentUpserts[$oid])
874 549
                    && ! isset($this->documentDeletions[$oid])
875 549
                    && isset($this->documentStates[$oid])
876 549
                ) {
877 261
                    $this->computeChangeSet($class, $document);
878 261
                }
879 553
            }
880 553
        }
881 553
    }
882
883
    /**
884
     * Computes the changes of an association.
885
     *
886
     * @param object $parentDocument
887
     * @param array $assoc
888
     * @param mixed $value The value of the association.
889
     * @throws \InvalidArgumentException
890
     */
891 419
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
892
    {
893 419
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
894 419
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
895 419
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
896
897 419
        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...
898 8
            return;
899
        }
900
901 418
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
902 226
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
903 222
                $this->scheduleCollectionUpdate($value);
904 222
            }
905 226
            $topmostOwner = $this->getOwningDocument($value->getOwner());
906 226
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
907 226
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
908 132
                $value->initialize();
909 132
                foreach ($value->getDeletedDocuments() as $orphan) {
910 21
                    $this->scheduleOrphanRemoval($orphan);
911 132
                }
912 132
            }
913 226
        }
914
915
        // Look through the documents, and in any of their associations,
916
        // for transient (new) documents, recursively. ("Persistence by reachability")
917
        // Unwrap. Uninitialized collections will simply be empty.
918 418
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
919
920 418
        $count = 0;
921 418
        foreach ($unwrappedValue as $key => $entry) {
922 323
            if ( ! is_object($entry)) {
923 1
                throw new \InvalidArgumentException(
924 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
925 1
                );
926
            }
927
928 322
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
929
930 322
            $state = $this->getDocumentState($entry, self::STATE_NEW);
931
932
            // Handle "set" strategy for multi-level hierarchy
933 322
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
934 322
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
935
936 322
            $count++;
937
938
            switch ($state) {
939 322
                case self::STATE_NEW:
940 56
                    if ( ! $assoc['isCascadePersist']) {
941
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
942
                            . " configured to cascade persist operations: " . self::objToStr($entry) . "."
943
                            . " Explicitly persist the new document or configure cascading persist operations"
944
                            . " on the relationship.");
945
                    }
946
947 56
                    $this->persistNew($targetClass, $entry);
948 56
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
949 56
                    $this->computeChangeSet($targetClass, $entry);
950 56
                    break;
951
952 318
                case self::STATE_MANAGED:
953 318
                    if ($targetClass->isEmbeddedDocument) {
954 161
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
955 161
                        if ($knownParent && $knownParent !== $parentDocument) {
956 6
                            $entry = clone $entry;
957 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
958 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
959 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
960 3
                            } else {
961
                                // must use unwrapped value to not trigger orphan removal
962 6
                                $unwrappedValue[$key] = $entry;
963
                            }
964 6
                            $this->persistNew($targetClass, $entry);
965 6
                        }
966 161
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
967 161
                        $this->computeChangeSet($targetClass, $entry);
968 161
                    }
969 318
                    break;
970
971 1
                case self::STATE_REMOVED:
972
                    // Consume the $value as array (it's either an array or an ArrayAccess)
973
                    // and remove the element from Collection.
974 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
975
                        unset($value[$key]);
976
                    }
977 1
                    break;
978
979
                case self::STATE_DETACHED:
980
                    // Can actually not happen right now as we assume STATE_NEW,
981
                    // so the exception will be raised from the DBAL layer (constraint violation).
982
                    throw new \InvalidArgumentException("A detached document was found through a "
983
                        . "relationship during cascading a persist operation.");
984
985
                default:
986
                    // MANAGED associated documents are already taken into account
987
                    // during changeset calculation anyway, since they are in the identity map.
988
989
            }
990 417
        }
991 417
    }
992
993
    /**
994
     * INTERNAL:
995
     * Computes the changeset of an individual document, independently of the
996
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
997
     *
998
     * The passed document must be a managed document. If the document already has a change set
999
     * because this method is invoked during a commit cycle then the change sets are added.
1000
     * whereby changes detected in this method prevail.
1001
     *
1002
     * @ignore
1003
     * @param ClassMetadata $class The class descriptor of the document.
1004
     * @param object $document The document for which to (re)calculate the change set.
1005
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1006
     */
1007 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1008
    {
1009
        // Ignore uninitialized proxy objects
1010 19
        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...
1011 1
            return;
1012
        }
1013
1014 18
        $oid = spl_object_hash($document);
1015
1016 18
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1017
            throw new \InvalidArgumentException('Document must be managed.');
1018
        }
1019
1020 18
        if ( ! $class->isInheritanceTypeNone()) {
1021 2
            $class = $this->dm->getClassMetadata(get_class($document));
1022 2
        }
1023
1024 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1025 18
    }
1026
1027
    /**
1028
     * @param ClassMetadata $class
1029
     * @param object $document
1030
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1031
     */
1032 574
    private function persistNew(ClassMetadata $class, $document)
1033
    {
1034 574
        $oid = spl_object_hash($document);
1035 574
        if ( ! empty($class->lifecycleCallbacks[Events::prePersist])) {
1036 156
            $class->invokeLifecycleCallbacks(Events::prePersist, $document);
1037 156
        }
1038 574 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...
1039 6
            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($document, $this->dm));
1040 6
        }
1041
1042 574
        $upsert = false;
1043 574
        if ($class->identifier) {
1044 574
            $idValue = $class->getIdentifierValue($document);
1045 574
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1046
1047 574
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1048 3
                throw new \InvalidArgumentException(sprintf(
1049 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1050 3
                    get_class($document)
1051 3
                ));
1052
            }
1053
1054
            // \MongoId::isValid($idValue) was introduced in 1.5.0 so it's no good
1055 573
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('/^[0-9a-f]{24}$/', $idValue)) {
1056 1
                throw new \InvalidArgumentException(sprintf(
1057 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1058 1
                    get_class($document)
1059 1
                ));
1060
            }
1061
1062 572
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1063 501
                $idValue = $class->idGenerator->generate($this->dm, $document);
1064 501
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1065 501
                $class->setIdentifierValue($document, $idValue);
1066 501
            }
1067
1068 572
            $this->documentIdentifiers[$oid] = $idValue;
1069 572
        } else {
1070
            // this is for embedded documents without identifiers
1071 142
            $this->documentIdentifiers[$oid] = $oid;
1072
        }
1073
1074 572
        $this->documentStates[$oid] = self::STATE_MANAGED;
1075
1076 572
        if ($upsert) {
1077 81
            $this->scheduleForUpsert($class, $document);
1078 81
        } else {
1079 506
            $this->scheduleForInsert($class, $document);
1080
        }
1081 572
    }
1082
1083
    /**
1084
     * Cascades the postPersist events to embedded documents.
1085
     *
1086
     * @param ClassMetadata $class
1087
     * @param object $document
1088
     */
1089 550
    private function cascadePostPersist(ClassMetadata $class, $document)
1090
    {
1091 550
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1092
1093 550
        $embeddedMappings = array_filter(
1094 550
            $class->associationMappings,
1095
            function($assoc) { return ! empty($assoc['embedded']); }
1096 550
        );
1097
1098 550
        foreach ($embeddedMappings as $mapping) {
1099 335
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1100
1101 335
            if ($value === null) {
1102 214
                continue;
1103
            }
1104
1105 317
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1106
1107 317
            if (isset($mapping['targetDocument'])) {
1108 306
                $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1109 306
            }
1110
1111 317
            foreach ($values as $embeddedDocument) {
1112 159
                if ( ! isset($mapping['targetDocument'])) {
1113 13
                    $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument));
1114 13
                }
1115
1116 159
                if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) {
1117 9
                    $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...
1118 9
                }
1119 159
                if ($hasPostPersistListeners) {
1120 4
                    $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm));
1121 4
                }
1122 159
                $this->cascadePostPersist($embeddedClass, $embeddedDocument);
1123 317
            }
1124 550
         }
1125 550
     }
1126
1127
    /**
1128
     * Executes all document insertions for documents of the specified type.
1129
     *
1130
     * @param ClassMetadata $class
1131
     * @param array $documents Array of documents to insert
1132
     * @param array $options Array of options to be used with batchInsert()
1133
     */
1134 485 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...
1135
    {
1136 485
        $persister = $this->getDocumentPersister($class->name);
1137
1138 485
        foreach ($documents as $oid => $document) {
1139 485
            $persister->addInsert($document);
1140 485
            unset($this->documentInsertions[$oid]);
1141 485
        }
1142
1143 485
        $persister->executeInserts($options);
1144
1145 484
        $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]);
1146 484
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1147
1148 484
        foreach ($documents as $document) {
1149 484
            if ($hasPostPersistLifecycleCallbacks) {
1150 9
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1151 9
            }
1152 484
            if ($hasPostPersistListeners) {
1153 5
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1154 5
            }
1155 484
            $this->cascadePostPersist($class, $document);
1156 484
        }
1157 484
    }
1158
1159
    /**
1160
     * Executes all document upserts for documents of the specified type.
1161
     *
1162
     * @param ClassMetadata $class
1163
     * @param array $documents Array of documents to upsert
1164
     * @param array $options Array of options to be used with batchInsert()
1165
     */
1166 78 View Code Duplication
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1167
    {
1168 78
        $persister = $this->getDocumentPersister($class->name);
1169
1170
1171 78
        foreach ($documents as $oid => $document) {
1172 78
            $persister->addUpsert($document);
1173 78
            unset($this->documentUpserts[$oid]);
1174 78
        }
1175
1176 78
        $persister->executeUpserts($options);
1177
1178 78
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
1179 78
        $hasListeners = $this->evm->hasListeners(Events::postPersist);
1180
1181 78
        foreach ($documents as $document) {
1182 78
            if ($hasLifecycleCallbacks) {
1183
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1184
            }
1185 78
            if ($hasListeners) {
1186 2
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1187 2
            }
1188 78
            $this->cascadePostPersist($class, $document);
1189 78
        }
1190 78
    }
1191
1192
    /**
1193
     * Executes all document updates for documents of the specified type.
1194
     *
1195
     * @param Mapping\ClassMetadata $class
1196
     * @param array $documents Array of documents to update
1197
     * @param array $options Array of options to be used with update()
1198
     */
1199 216
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1200
    {
1201 216
        $className = $class->name;
1202 216
        $persister = $this->getDocumentPersister($className);
1203
1204 216
        $hasPreUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::preUpdate]);
1205 216
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1206 216
        $hasPostUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postUpdate]);
1207 216
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1208
1209 216
        foreach ($documents as $oid => $document) {
1210 216
            if ($hasPreUpdateLifecycleCallbacks) {
1211 11
                $class->invokeLifecycleCallbacks(Events::preUpdate, $document);
1212 11
                $this->recomputeSingleDocumentChangeSet($class, $document);
1213 11
            }
1214
1215 216
            if ($hasPreUpdateListeners) {
1216 8
                if ( ! isset($this->documentChangeSets[$oid])) {
1217
                    // only ReferenceMany collection is scheduled for update
1218 1
                    $this->documentChangeSets[$oid] = array();
1219 1
                }
1220 8
                $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1221 8
                    $document, $this->dm, $this->documentChangeSets[$oid])
1222 8
                );
1223 8
            }
1224 216
            $this->cascadePreUpdate($class, $document);
1225
1226 216
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1227 214
                $persister->update($document, $options);
1228 210
            }
1229
1230 212
            unset($this->documentUpdates[$oid]);
1231
1232 212
            if ($hasPostUpdateLifecycleCallbacks) {
1233 6
                $class->invokeLifecycleCallbacks(Events::postUpdate, $document);
1234 6
            }
1235 212
            if ($hasPostUpdateListeners) {
1236 8
                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm));
1237 8
            }
1238 212
            $this->cascadePostUpdate($class, $document);
1239 212
        }
1240 211
    }
1241
1242
    /**
1243
     * Cascades the preUpdate event to embedded documents.
1244
     *
1245
     * @param ClassMetadata $class
1246
     * @param object $document
1247
     */
1248 216
    private function cascadePreUpdate(ClassMetadata $class, $document)
1249
    {
1250 216
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1251
1252 216
        $embeddedMappings = array_filter(
1253 216
            $class->associationMappings,
1254
            function ($assoc) { return ! empty($assoc['embedded']); }
1255 216
        );
1256
1257 216
        foreach ($embeddedMappings as $mapping) {
1258 133
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1259
1260 133
            if ($value === null) {
1261 49
                continue;
1262
            }
1263
1264 131
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1265
1266 131
            foreach ($values as $entry) {
1267 84
                $entryOid = spl_object_hash($entry);
1268 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1269
1270 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1271 47
                    continue;
1272
                }
1273
1274 67
                if (isset($this->documentInsertions[$entryOid])) {
1275 52
                    continue;
1276
                }
1277
1278 45 View Code Duplication
                if ( ! empty($entryClass->lifecycleCallbacks[Events::preUpdate])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1279 5
                    $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry);
1280 5
                    $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
1281 5
                }
1282 45
                if ($hasPreUpdateListeners) {
1283 3
                    $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1284 3
                        $entry, $this->dm, $this->documentChangeSets[$entryOid])
1285 3
                    );
1286 3
                }
1287
1288 45
                $this->cascadePreUpdate($entryClass, $entry);
1289 131
            }
1290 216
        }
1291 216
    }
1292
1293
    /**
1294
     * Cascades the postUpdate and postPersist events to embedded documents.
1295
     *
1296
     * @param ClassMetadata $class
1297
     * @param object $document
1298
     */
1299 212
    private function cascadePostUpdate(ClassMetadata $class, $document)
1300
    {
1301 212
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1302 212
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1303
1304 212
        $embeddedMappings = array_filter(
1305 212
            $class->associationMappings,
1306
            function($assoc) { return ! empty($assoc['embedded']); }
1307 212
        );
1308
1309 212
        foreach ($embeddedMappings as $mapping) {
1310 129
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1311
1312 129
            if ($value === null) {
1313 52
                continue;
1314
            }
1315
1316 127
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1317
1318 127
            foreach ($values as $entry) {
1319 84
                $entryOid = spl_object_hash($entry);
1320 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1321
1322 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1323 47
                    continue;
1324
                }
1325
1326 67
                if (isset($this->documentInsertions[$entryOid])) {
1327 52
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) {
1328 1
                        $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry);
1329 1
                    }
1330 52
                    if ($hasPostPersistListeners) {
1331 3
                        $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm));
1332 3
                    }
1333 52
                } else {
1334 45
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) {
1335 9
                        $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry);
1336 9
                    }
1337 45
                    if ($hasPostUpdateListeners) {
1338 3
                        $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm));
1339 3
                    }
1340
                }
1341
1342 67
                $this->cascadePostUpdate($entryClass, $entry);
1343 127
            }
1344 212
        }
1345 212
    }
1346
1347
    /**
1348
     * Executes all document deletions for documents of the specified type.
1349
     *
1350
     * @param ClassMetadata $class
1351
     * @param array $documents Array of documents to delete
1352
     * @param array $options Array of options to be used with remove()
1353
     */
1354 62
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1355
    {
1356 62
        $hasPostRemoveLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postRemove]);
1357 62
        $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove);
1358
1359 62
        $persister = $this->getDocumentPersister($class->name);
1360
1361 62
        foreach ($documents as $oid => $document) {
1362 62
            if ( ! $class->isEmbeddedDocument) {
1363 28
                $persister->delete($document, $options);
1364 26
            }
1365
            unset(
1366 60
                $this->documentDeletions[$oid],
1367 60
                $this->documentIdentifiers[$oid],
1368 60
                $this->originalDocumentData[$oid]
1369
            );
1370
1371
            // Clear snapshot information for any referenced PersistentCollection
1372
            // http://www.doctrine-project.org/jira/browse/MODM-95
1373 60
            foreach ($class->associationMappings as $fieldMapping) {
1374 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1375 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1376 26
                    if ($value instanceof PersistentCollection) {
1377 22
                        $value->clearSnapshot();
1378 22
                    }
1379 26
                }
1380 60
            }
1381
1382
            // Document with this $oid after deletion treated as NEW, even if the $oid
1383
            // is obtained by a new document because the old one went out of scope.
1384 60
            $this->documentStates[$oid] = self::STATE_NEW;
1385
1386 60
            if ($hasPostRemoveLifecycleCallbacks) {
1387 8
                $class->invokeLifecycleCallbacks(Events::postRemove, $document);
1388 8
            }
1389 60
            if ($hasPostRemoveListeners) {
1390 2
                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm));
1391 2
            }
1392 60
        }
1393 60
    }
1394
1395
    /**
1396
     * Schedules a document for insertion into the database.
1397
     * If the document already has an identifier, it will be added to the
1398
     * identity map.
1399
     *
1400
     * @param ClassMetadata $class
1401
     * @param object $document The document to schedule for insertion.
1402
     * @throws \InvalidArgumentException
1403
     */
1404 509
    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...
1405
    {
1406 509
        $oid = spl_object_hash($document);
1407
1408 509
        if (isset($this->documentUpdates[$oid])) {
1409
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1410
        }
1411 509
        if (isset($this->documentDeletions[$oid])) {
1412
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1413
        }
1414 509
        if (isset($this->documentInsertions[$oid])) {
1415
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1416
        }
1417
1418 509
        $this->documentInsertions[$oid] = $document;
1419
1420 509
        if (isset($this->documentIdentifiers[$oid])) {
1421 506
            $this->addToIdentityMap($document);
1422 506
        }
1423 509
    }
1424
1425
    /**
1426
     * Schedules a document for upsert into the database and adds it to the
1427
     * identity map
1428
     *
1429
     * @param ClassMetadata $class
1430
     * @param object $document The document to schedule for upsert.
1431
     * @throws \InvalidArgumentException
1432
     */
1433 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1434
    {
1435 84
        $oid = spl_object_hash($document);
1436
1437 84
        if ($class->isEmbeddedDocument) {
1438
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1439
        }
1440 84
        if (isset($this->documentUpdates[$oid])) {
1441
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1442
        }
1443 84
        if (isset($this->documentDeletions[$oid])) {
1444
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1445
        }
1446 84
        if (isset($this->documentUpserts[$oid])) {
1447
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1448
        }
1449
1450 84
        $this->documentUpserts[$oid] = $document;
1451 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1452 84
        $this->addToIdentityMap($document);
1453 84
    }
1454
1455
    /**
1456
     * Checks whether a document is scheduled for insertion.
1457
     *
1458
     * @param object $document
1459
     * @return boolean
1460
     */
1461 70
    public function isScheduledForInsert($document)
1462
    {
1463 70
        return isset($this->documentInsertions[spl_object_hash($document)]);
1464
    }
1465
1466
    /**
1467
     * Checks whether a document is scheduled for upsert.
1468
     *
1469
     * @param object $document
1470
     * @return boolean
1471
     */
1472 5
    public function isScheduledForUpsert($document)
1473
    {
1474 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1475
    }
1476
1477
    /**
1478
     * Schedules a document for being updated.
1479
     *
1480
     * @param object $document The document to schedule for being updated.
1481
     * @throws \InvalidArgumentException
1482
     */
1483 225
    public function scheduleForUpdate($document)
1484
    {
1485 225
        $oid = spl_object_hash($document);
1486 225
        if ( ! isset($this->documentIdentifiers[$oid])) {
1487
            throw new \InvalidArgumentException("Document has no identity.");
1488
        }
1489
1490 225
        if (isset($this->documentDeletions[$oid])) {
1491
            throw new \InvalidArgumentException("Document is removed.");
1492
        }
1493
1494 225
        if ( ! isset($this->documentUpdates[$oid])
1495 225
            && ! isset($this->documentInsertions[$oid])
1496 225
            && ! isset($this->documentUpserts[$oid])) {
1497 221
            $this->documentUpdates[$oid] = $document;
1498 221
        }
1499 225
    }
1500
1501
    /**
1502
     * Checks whether a document is registered as dirty in the unit of work.
1503
     * Note: Is not very useful currently as dirty documents are only registered
1504
     * at commit time.
1505
     *
1506
     * @param object $document
1507
     * @return boolean
1508
     */
1509 13
    public function isScheduledForUpdate($document)
1510
    {
1511 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1512
    }
1513
1514 1
    public function isScheduledForDirtyCheck($document)
1515
    {
1516 1
        $class = $this->dm->getClassMetadata(get_class($document));
1517 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1518
    }
1519
1520
    /**
1521
     * INTERNAL:
1522
     * Schedules a document for deletion.
1523
     *
1524
     * @param object $document
1525
     */
1526 67
    public function scheduleForDelete($document)
1527
    {
1528 67
        $oid = spl_object_hash($document);
1529
1530 67
        if (isset($this->documentInsertions[$oid])) {
1531 2
            if ($this->isInIdentityMap($document)) {
1532 2
                $this->removeFromIdentityMap($document);
1533 2
            }
1534 2
            unset($this->documentInsertions[$oid]);
1535 2
            return; // document has not been persisted yet, so nothing more to do.
1536
        }
1537
1538 66
        if ( ! $this->isInIdentityMap($document)) {
1539 1
            return; // ignore
1540
        }
1541
1542 65
        $this->removeFromIdentityMap($document);
1543 65
        $this->documentStates[$oid] = self::STATE_REMOVED;
1544
1545 65
        if (isset($this->documentUpdates[$oid])) {
1546
            unset($this->documentUpdates[$oid]);
1547
        }
1548 65
        if ( ! isset($this->documentDeletions[$oid])) {
1549 65
            $this->documentDeletions[$oid] = $document;
1550 65
        }
1551 65
    }
1552
1553
    /**
1554
     * Checks whether a document is registered as removed/deleted with the unit
1555
     * of work.
1556
     *
1557
     * @param object $document
1558
     * @return boolean
1559
     */
1560 8
    public function isScheduledForDelete($document)
1561
    {
1562 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1563
    }
1564
1565
    /**
1566
     * Checks whether a document is scheduled for insertion, update or deletion.
1567
     *
1568
     * @param $document
1569
     * @return boolean
1570
     */
1571 225
    public function isDocumentScheduled($document)
1572
    {
1573 225
        $oid = spl_object_hash($document);
1574 225
        return isset($this->documentInsertions[$oid]) ||
1575 121
            isset($this->documentUpserts[$oid]) ||
1576 112
            isset($this->documentUpdates[$oid]) ||
1577 225
            isset($this->documentDeletions[$oid]);
1578
    }
1579
1580
    /**
1581
     * INTERNAL:
1582
     * Registers a document in the identity map.
1583
     *
1584
     * Note that documents in a hierarchy are registered with the class name of
1585
     * the root document. Identifiers are serialized before being used as array
1586
     * keys to allow differentiation of equal, but not identical, values.
1587
     *
1588
     * @ignore
1589
     * @param object $document  The document to register.
1590
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1591
     *                  the document in question is already managed.
1592
     */
1593 601
    public function addToIdentityMap($document)
1594
    {
1595 601
        $class = $this->dm->getClassMetadata(get_class($document));
1596 601
        $id = $this->getIdForIdentityMap($document);
1597
1598 601
        if (isset($this->identityMap[$class->name][$id])) {
1599 53
            return false;
1600
        }
1601
1602 601
        $this->identityMap[$class->name][$id] = $document;
1603
1604 601
        if ($document instanceof NotifyPropertyChanged &&
1605 601
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1606 3
            $document->addPropertyChangedListener($this);
1607 3
        }
1608
1609 601
        return true;
1610
    }
1611
1612
    /**
1613
     * Gets the state of a document with regard to the current unit of work.
1614
     *
1615
     * @param object   $document
1616
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1617
     *                         This parameter can be set to improve performance of document state detection
1618
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1619
     *                         is either known or does not matter for the caller of the method.
1620
     * @return int The document state.
1621
     */
1622 577
    public function getDocumentState($document, $assume = null)
1623
    {
1624 577
        $oid = spl_object_hash($document);
1625
1626 577
        if (isset($this->documentStates[$oid])) {
1627 353
            return $this->documentStates[$oid];
1628
        }
1629
1630 577
        $class = $this->dm->getClassMetadata(get_class($document));
1631
1632 577
        if ($class->isEmbeddedDocument) {
1633 175
            return self::STATE_NEW;
1634
        }
1635
1636 574
        if ($assume !== null) {
1637 571
            return $assume;
1638
        }
1639
1640
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1641
         * known. Note that you cannot remember the NEW or DETACHED state in
1642
         * _documentStates since the UoW does not hold references to such
1643
         * objects and the object hash can be reused. More generally, because
1644
         * the state may "change" between NEW/DETACHED without the UoW being
1645
         * aware of it.
1646
         */
1647 4
        $id = $class->getIdentifierObject($document);
1648
1649 4
        if ($id === null) {
1650 2
            return self::STATE_NEW;
1651
        }
1652
1653
        // Check for a version field, if available, to avoid a DB lookup.
1654 2
        if ($class->isVersioned) {
1655
            return ($class->getFieldValue($document, $class->versionField))
1656
                ? self::STATE_DETACHED
1657
                : self::STATE_NEW;
1658
        }
1659
1660
        // Last try before DB lookup: check the identity map.
1661 2
        if ($this->tryGetById($id, $class)) {
1662 1
            return self::STATE_DETACHED;
1663
        }
1664
1665
        // DB lookup
1666 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1667 1
            return self::STATE_DETACHED;
1668
        }
1669
1670 1
        return self::STATE_NEW;
1671
    }
1672
1673
    /**
1674
     * INTERNAL:
1675
     * Removes a document from the identity map. This effectively detaches the
1676
     * document from the persistence management of Doctrine.
1677
     *
1678
     * @ignore
1679
     * @param object $document
1680
     * @throws \InvalidArgumentException
1681
     * @return boolean
1682
     */
1683 76
    public function removeFromIdentityMap($document)
1684
    {
1685 76
        $oid = spl_object_hash($document);
1686
1687
        // Check if id is registered first
1688 76
        if ( ! isset($this->documentIdentifiers[$oid])) {
1689
            return false;
1690
        }
1691
1692 76
        $class = $this->dm->getClassMetadata(get_class($document));
1693 76
        $id = $this->getIdForIdentityMap($document);
1694
1695 76
        if (isset($this->identityMap[$class->name][$id])) {
1696 76
            unset($this->identityMap[$class->name][$id]);
1697 76
            $this->documentStates[$oid] = self::STATE_DETACHED;
1698 76
            return true;
1699
        }
1700
1701
        return false;
1702
    }
1703
1704
    /**
1705
     * INTERNAL:
1706
     * Gets a document in the identity map by its identifier hash.
1707
     *
1708
     * @ignore
1709
     * @param mixed         $id    Document identifier
1710
     * @param ClassMetadata $class Document class
1711
     * @return object
1712
     * @throws InvalidArgumentException if the class does not have an identifier
1713
     */
1714 31
    public function getById($id, ClassMetadata $class)
1715
    {
1716 31
        if ( ! $class->identifier) {
1717
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1718
        }
1719
1720 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1721
1722 31
        return $this->identityMap[$class->name][$serializedId];
1723
    }
1724
1725
    /**
1726
     * INTERNAL:
1727
     * Tries to get a document by its identifier hash. If no document is found
1728
     * for the given hash, FALSE is returned.
1729
     *
1730
     * @ignore
1731
     * @param mixed         $id    Document identifier
1732
     * @param ClassMetadata $class Document class
1733
     * @return mixed The found document or FALSE.
1734
     * @throws InvalidArgumentException if the class does not have an identifier
1735
     */
1736 290
    public function tryGetById($id, ClassMetadata $class)
1737
    {
1738 290
        if ( ! $class->identifier) {
1739
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1740
        }
1741
1742 290
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1743
1744 290
        return isset($this->identityMap[$class->name][$serializedId]) ?
1745 290
            $this->identityMap[$class->name][$serializedId] : false;
1746
    }
1747
1748
    /**
1749
     * Schedules a document for dirty-checking at commit-time.
1750
     *
1751
     * @param object $document The document to schedule for dirty-checking.
1752
     * @todo Rename: scheduleForSynchronization
1753
     */
1754 2
    public function scheduleForDirtyCheck($document)
1755
    {
1756 2
        $class = $this->dm->getClassMetadata(get_class($document));
1757 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1758 2
    }
1759
1760
    /**
1761
     * Checks whether a document is registered in the identity map.
1762
     *
1763
     * @param object $document
1764
     * @return boolean
1765
     */
1766 76
    public function isInIdentityMap($document)
1767
    {
1768 76
        $oid = spl_object_hash($document);
1769
1770 76
        if ( ! isset($this->documentIdentifiers[$oid])) {
1771 4
            return false;
1772
        }
1773
1774 75
        $class = $this->dm->getClassMetadata(get_class($document));
1775 75
        $id = $this->getIdForIdentityMap($document);
1776
1777 75
        return isset($this->identityMap[$class->name][$id]);
1778
    }
1779
1780
    /**
1781
     * @param object $document
1782
     * @return string
1783
     */
1784 601
    private function getIdForIdentityMap($document)
1785
    {
1786 601
        $class = $this->dm->getClassMetadata(get_class($document));
1787
1788 601
        if ( ! $class->identifier) {
1789 145
            $id = spl_object_hash($document);
1790 145
        } else {
1791 600
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1792 600
            $id = serialize($class->getDatabaseIdentifierValue($id));
1793
        }
1794
1795 601
        return $id;
1796
    }
1797
1798
    /**
1799
     * INTERNAL:
1800
     * Checks whether an identifier exists in the identity map.
1801
     *
1802
     * @ignore
1803
     * @param string $id
1804
     * @param string $rootClassName
1805
     * @return boolean
1806
     */
1807
    public function containsId($id, $rootClassName)
1808
    {
1809
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1810
    }
1811
1812
    /**
1813
     * Persists a document as part of the current unit of work.
1814
     *
1815
     * @param object $document The document to persist.
1816
     * @throws MongoDBException If trying to persist MappedSuperclass.
1817
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1818
     */
1819 572
    public function persist($document)
1820
    {
1821 572
        $class = $this->dm->getClassMetadata(get_class($document));
1822 572
        if ($class->isMappedSuperclass) {
1823 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1824
        }
1825 571
        $visited = array();
1826 571
        $this->doPersist($document, $visited);
1827 567
    }
1828
1829
    /**
1830
     * Saves a document as part of the current unit of work.
1831
     * This method is internally called during save() cascades as it tracks
1832
     * the already visited documents to prevent infinite recursions.
1833
     *
1834
     * NOTE: This method always considers documents that are not yet known to
1835
     * this UnitOfWork as NEW.
1836
     *
1837
     * @param object $document The document to persist.
1838
     * @param array $visited The already visited documents.
1839
     * @throws \InvalidArgumentException
1840
     * @throws MongoDBException
1841
     */
1842 571
    private function doPersist($document, array &$visited)
1843
    {
1844 571
        $oid = spl_object_hash($document);
1845 571
        if (isset($visited[$oid])) {
1846 24
            return; // Prevent infinite recursion
1847
        }
1848
1849 571
        $visited[$oid] = $document; // Mark visited
1850
1851 571
        $class = $this->dm->getClassMetadata(get_class($document));
1852
1853 571
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1854
        switch ($documentState) {
1855 571
            case self::STATE_MANAGED:
1856
                // Nothing to do, except if policy is "deferred explicit"
1857 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1858
                    $this->scheduleForDirtyCheck($document);
1859
                }
1860 44
                break;
1861 571
            case self::STATE_NEW:
1862 571
                $this->persistNew($class, $document);
1863 569
                break;
1864
1865 2
            case self::STATE_REMOVED:
1866
                // Document becomes managed again
1867 2
                unset($this->documentDeletions[$oid]);
1868
1869 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1870 2
                break;
1871
1872
            case self::STATE_DETACHED:
1873
                throw new \InvalidArgumentException(
1874
                    "Behavior of persist() for a detached document is not yet defined.");
1875
1876
            default:
1877
                throw MongoDBException::invalidDocumentState($documentState);
1878
        }
1879
1880 569
        $this->cascadePersist($document, $visited);
1881 567
    }
1882
1883
    /**
1884
     * Deletes a document as part of the current unit of work.
1885
     *
1886
     * @param object $document The document to remove.
1887
     */
1888 66
    public function remove($document)
1889
    {
1890 66
        $visited = array();
1891 66
        $this->doRemove($document, $visited);
1892 66
    }
1893
1894
    /**
1895
     * Deletes a document as part of the current unit of work.
1896
     *
1897
     * This method is internally called during delete() cascades as it tracks
1898
     * the already visited documents to prevent infinite recursions.
1899
     *
1900
     * @param object $document The document to delete.
1901
     * @param array $visited The map of the already visited documents.
1902
     * @throws MongoDBException
1903
     */
1904 66
    private function doRemove($document, array &$visited)
1905
    {
1906 66
        $oid = spl_object_hash($document);
1907 66
        if (isset($visited[$oid])) {
1908 1
            return; // Prevent infinite recursion
1909
        }
1910
1911 66
        $visited[$oid] = $document; // mark visited
1912
1913
        /* Cascade first, because scheduleForDelete() removes the entity from
1914
         * the identity map, which can cause problems when a lazy Proxy has to
1915
         * be initialized for the cascade operation.
1916
         */
1917 66
        $this->cascadeRemove($document, $visited);
1918
1919 66
        $class = $this->dm->getClassMetadata(get_class($document));
1920 66
        $documentState = $this->getDocumentState($document);
1921
        switch ($documentState) {
1922 66
            case self::STATE_NEW:
1923 66
            case self::STATE_REMOVED:
1924
                // nothing to do
1925 1
                break;
1926 66
            case self::STATE_MANAGED:
1927 66
                if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) {
1928 8
                    $class->invokeLifecycleCallbacks(Events::preRemove, $document);
1929 8
                }
1930 66 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...
1931 1
                    $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm));
1932 1
                }
1933 66
                $this->scheduleForDelete($document);
1934 66
                break;
1935
            case self::STATE_DETACHED:
1936
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1937
            default:
1938
                throw MongoDBException::invalidDocumentState($documentState);
1939
        }
1940 66
    }
1941
1942
    /**
1943
     * Merges the state of the given detached document into this UnitOfWork.
1944
     *
1945
     * @param object $document
1946
     * @return object The managed copy of the document.
1947
     */
1948 13
    public function merge($document)
1949
    {
1950 13
        $visited = array();
1951
1952 13
        return $this->doMerge($document, $visited);
1953
    }
1954
1955
    /**
1956
     * Executes a merge operation on a document.
1957
     *
1958
     * @param object      $document
1959
     * @param array       $visited
1960
     * @param object|null $prevManagedCopy
1961
     * @param array|null  $assoc
1962
     *
1963
     * @return object The managed copy of the document.
1964
     *
1965
     * @throws InvalidArgumentException If the entity instance is NEW.
1966
     * @throws LockException If the document uses optimistic locking through a
1967
     *                       version attribute and the version check against the
1968
     *                       managed copy fails.
1969
     */
1970 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1971
    {
1972 13
        $oid = spl_object_hash($document);
1973
1974 13
        if (isset($visited[$oid])) {
1975 1
            return $visited[$oid]; // Prevent infinite recursion
1976
        }
1977
1978 13
        $visited[$oid] = $document; // mark visited
1979
1980 13
        $class = $this->dm->getClassMetadata(get_class($document));
1981
1982
        /* First we assume DETACHED, although it can still be NEW but we can
1983
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1984
         * an identity, we need to fetch it from the DB anyway in order to
1985
         * merge. MANAGED documents are ignored by the merge operation.
1986
         */
1987 13
        $managedCopy = $document;
1988
1989 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1990 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1991
                $document->__load();
1992
            }
1993
1994
            // Try to look the document up in the identity map.
1995 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1996
1997 13
            if ($id === null) {
1998
                // If there is no identifier, it is actually NEW.
1999 5
                $managedCopy = $class->newInstance();
2000 5
                $this->persistNew($class, $managedCopy);
2001 5
            } else {
2002 10
                $managedCopy = $this->tryGetById($id, $class);
2003
2004 10
                if ($managedCopy) {
2005
                    // We have the document in memory already, just make sure it is not removed.
2006 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
2007
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
2008
                    }
2009 5
                } else {
2010
                    // We need to fetch the managed copy in order to merge.
2011 7
                    $managedCopy = $this->dm->find($class->name, $id);
2012
                }
2013
2014 10
                if ($managedCopy === null) {
2015
                    // If the identifier is ASSIGNED, it is NEW
2016
                    $managedCopy = $class->newInstance();
2017
                    $class->setIdentifierValue($managedCopy, $id);
2018
                    $this->persistNew($class, $managedCopy);
2019
                } else {
2020 10
                    if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2021
                        $managedCopy->__load();
2022
                    }
2023
                }
2024
            }
2025
2026 13
            if ($class->isVersioned) {
2027
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
2028
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2029
2030
                // Throw exception if versions don't match
2031
                if ($managedCopyVersion != $documentVersion) {
2032
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
2033
                }
2034
            }
2035
2036
            // Merge state of $document into existing (managed) document
2037 13
            foreach ($class->reflClass->getProperties() as $prop) {
2038 13
                $name = $prop->name;
2039 13
                $prop->setAccessible(true);
2040 13
                if ( ! isset($class->associationMappings[$name])) {
2041 13
                    if ( ! $class->isIdentifier($name)) {
2042 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
2043 13
                    }
2044 13
                } else {
2045 13
                    $assoc2 = $class->associationMappings[$name];
2046
2047 13
                    if ($assoc2['type'] === 'one') {
2048 5
                        $other = $prop->getValue($document);
2049
2050 5
                        if ($other === null) {
2051 2
                            $prop->setValue($managedCopy, null);
2052 5
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2053
                            // Do not merge fields marked lazy that have not been fetched
2054 1
                            continue;
2055 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
2056
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
2057
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
2058
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
2059
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
2060
                                $relatedId = $targetClass->getIdentifierObject($other);
2061
2062
                                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...
2063
                                    $other = $this->dm->find($targetClass->name, $relatedId);
2064
                                } else {
2065
                                    $other = $this
2066
                                        ->dm
2067
                                        ->getProxyFactory()
2068
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
2069
                                    $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...
2070
                                }
2071
                            }
2072
2073
                            $prop->setValue($managedCopy, $other);
2074
                        }
2075 4
                    } else {
2076 10
                        $mergeCol = $prop->getValue($document);
2077
2078 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
2079
                            /* Do not merge fields marked lazy that have not
2080
                             * been fetched. Keep the lazy persistent collection
2081
                             * of the managed copy.
2082
                             */
2083 3
                            continue;
2084
                        }
2085
2086 7
                        $managedCol = $prop->getValue($managedCopy);
2087
2088 7
                        if ( ! $managedCol) {
2089 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
2090 2
                            $managedCol->setOwner($managedCopy, $assoc2);
2091 2
                            $prop->setValue($managedCopy, $managedCol);
2092 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
2093 2
                        }
2094
2095
                        /* Note: do not process association's target documents.
2096
                         * They will be handled during the cascade. Initialize
2097
                         * and, if necessary, clear $managedCol for now.
2098
                         */
2099 7
                        if ($assoc2['isCascadeMerge']) {
2100 7
                            $managedCol->initialize();
2101
2102
                            // If $managedCol differs from the merged collection, clear and set dirty
2103 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
2104 2
                                $managedCol->unwrap()->clear();
2105 2
                                $managedCol->setDirty(true);
2106
2107 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
2108
                                    $this->scheduleForDirtyCheck($managedCopy);
2109
                                }
2110 2
                            }
2111 7
                        }
2112
                    }
2113
                }
2114
2115 13
                if ($class->isChangeTrackingNotify()) {
2116
                    // Just treat all properties as changed, there is no other choice.
2117
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2118
                }
2119 13
            }
2120
2121 13
            if ($class->isChangeTrackingDeferredExplicit()) {
2122
                $this->scheduleForDirtyCheck($document);
2123
            }
2124 13
        }
2125
2126 13
        if ($prevManagedCopy !== null) {
2127 6
            $assocField = $assoc['fieldName'];
2128 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
2129
2130 6
            if ($assoc['type'] === 'one') {
2131 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
2132 2
            } else {
2133 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
2134
2135 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
2136 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
2137 1
                }
2138
            }
2139 6
        }
2140
2141
        // Mark the managed copy visited as well
2142 13
        $visited[spl_object_hash($managedCopy)] = true;
2143
2144 13
        $this->cascadeMerge($document, $managedCopy, $visited);
2145
2146 13
        return $managedCopy;
2147
    }
2148
2149
    /**
2150
     * Detaches a document from the persistence management. It's persistence will
2151
     * no longer be managed by Doctrine.
2152
     *
2153
     * @param object $document The document to detach.
2154
     */
2155 9
    public function detach($document)
2156
    {
2157 9
        $visited = array();
2158 9
        $this->doDetach($document, $visited);
2159 9
    }
2160
2161
    /**
2162
     * Executes a detach operation on the given document.
2163
     *
2164
     * @param object $document
2165
     * @param array $visited
2166
     * @internal This method always considers documents with an assigned identifier as DETACHED.
2167
     */
2168 12
    private function doDetach($document, array &$visited)
2169
    {
2170 12
        $oid = spl_object_hash($document);
2171 12
        if (isset($visited[$oid])) {
2172 4
            return; // Prevent infinite recursion
2173
        }
2174
2175 12
        $visited[$oid] = $document; // mark visited
2176
2177 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2178 12
            case self::STATE_MANAGED:
2179 12
                $this->removeFromIdentityMap($document);
2180 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2181 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2182 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2183 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2184 12
                    $this->hasScheduledCollections[$oid]);
2185 12
                break;
2186 4
            case self::STATE_NEW:
2187 4
            case self::STATE_DETACHED:
2188 4
                return;
2189 12
        }
2190
2191 12
        $this->cascadeDetach($document, $visited);
2192 12
    }
2193
2194
    /**
2195
     * Refreshes the state of the given document from the database, overwriting
2196
     * any local, unpersisted changes.
2197
     *
2198
     * @param object $document The document to refresh.
2199
     * @throws \InvalidArgumentException If the document is not MANAGED.
2200
     */
2201 21
    public function refresh($document)
2202
    {
2203 21
        $visited = array();
2204 21
        $this->doRefresh($document, $visited);
2205 20
    }
2206
2207
    /**
2208
     * Executes a refresh operation on a document.
2209
     *
2210
     * @param object $document The document to refresh.
2211
     * @param array $visited The already visited documents during cascades.
2212
     * @throws \InvalidArgumentException If the document is not MANAGED.
2213
     */
2214 21
    private function doRefresh($document, array &$visited)
2215
    {
2216 21
        $oid = spl_object_hash($document);
2217 21
        if (isset($visited[$oid])) {
2218
            return; // Prevent infinite recursion
2219
        }
2220
2221 21
        $visited[$oid] = $document; // mark visited
2222
2223 21
        $class = $this->dm->getClassMetadata(get_class($document));
2224
2225 21
        if ( ! $class->isEmbeddedDocument) {
2226 21
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2227 20
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2228 20
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2229 20
            } else {
2230 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2231
            }
2232 20
        }
2233
2234 20
        $this->cascadeRefresh($document, $visited);
2235 20
    }
2236
2237
    /**
2238
     * Cascades a refresh operation to associated documents.
2239
     *
2240
     * @param object $document
2241
     * @param array $visited
2242
     */
2243 20
    private function cascadeRefresh($document, array &$visited)
2244
    {
2245 20
        $class = $this->dm->getClassMetadata(get_class($document));
2246
2247 20
        $associationMappings = array_filter(
2248 20
            $class->associationMappings,
2249
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2250 20
        );
2251
2252 20
        foreach ($associationMappings as $mapping) {
2253 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2254 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2255 15
                if ($relatedDocuments instanceof PersistentCollection) {
2256
                    // Unwrap so that foreach() does not initialize
2257 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2258 15
                }
2259 15
                foreach ($relatedDocuments as $relatedDocument) {
2260
                    $this->doRefresh($relatedDocument, $visited);
2261 15
                }
2262 15
            } elseif ($relatedDocuments !== null) {
2263 2
                $this->doRefresh($relatedDocuments, $visited);
2264 2
            }
2265 20
        }
2266 20
    }
2267
2268
    /**
2269
     * Cascades a detach operation to associated documents.
2270
     *
2271
     * @param object $document
2272
     * @param array $visited
2273
     */
2274 12 View Code Duplication
    private function cascadeDetach($document, array &$visited)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2275
    {
2276 12
        $class = $this->dm->getClassMetadata(get_class($document));
2277 12
        foreach ($class->fieldMappings as $mapping) {
2278 12
            if ( ! $mapping['isCascadeDetach']) {
2279 12
                continue;
2280
            }
2281 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2282 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2283 7
                if ($relatedDocuments instanceof PersistentCollection) {
2284
                    // Unwrap so that foreach() does not initialize
2285 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2286 6
                }
2287 7
                foreach ($relatedDocuments as $relatedDocument) {
2288 5
                    $this->doDetach($relatedDocument, $visited);
2289 7
                }
2290 7
            } elseif ($relatedDocuments !== null) {
2291 5
                $this->doDetach($relatedDocuments, $visited);
2292 5
            }
2293 12
        }
2294 12
    }
2295
    /**
2296
     * Cascades a merge operation to associated documents.
2297
     *
2298
     * @param object $document
2299
     * @param object $managedCopy
2300
     * @param array $visited
2301
     */
2302 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2303
    {
2304 13
        $class = $this->dm->getClassMetadata(get_class($document));
2305
2306 13
        $associationMappings = array_filter(
2307 13
            $class->associationMappings,
2308
            function ($assoc) { return $assoc['isCascadeMerge']; }
2309 13
        );
2310
2311 13
        foreach ($associationMappings as $assoc) {
2312 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2313
2314 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2315 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2316
                    // Collections are the same, so there is nothing to do
2317
                    continue;
2318
                }
2319
2320 8
                if ($relatedDocuments instanceof PersistentCollection) {
2321
                    // Unwrap so that foreach() does not initialize
2322 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2323 6
                }
2324
2325 8
                foreach ($relatedDocuments as $relatedDocument) {
2326 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2327 8
                }
2328 12
            } elseif ($relatedDocuments !== null) {
2329 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2330 3
            }
2331 13
        }
2332 13
    }
2333
2334
    /**
2335
     * Cascades the save operation to associated documents.
2336
     *
2337
     * @param object $document
2338
     * @param array $visited
2339
     */
2340 569
    private function cascadePersist($document, array &$visited)
2341
    {
2342 569
        $class = $this->dm->getClassMetadata(get_class($document));
2343
2344 569
        $associationMappings = array_filter(
2345 569
            $class->associationMappings,
2346
            function ($assoc) { return $assoc['isCascadePersist']; }
2347 569
        );
2348
2349 569
        foreach ($associationMappings as $fieldName => $mapping) {
2350 390
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2351
2352 390
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2353 340
                if ($relatedDocuments instanceof PersistentCollection) {
2354 17
                    if ($relatedDocuments->getOwner() !== $document) {
2355 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2356 2
                    }
2357
                    // Unwrap so that foreach() does not initialize
2358 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2359 17
                }
2360
2361 340
                $count = 0;
2362 340
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2363 187
                    if ( ! empty($mapping['embedded'])) {
2364 113
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2365 113
                        if ($knownParent && $knownParent !== $document) {
2366 4
                            $relatedDocument = clone $relatedDocument;
2367 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2368 4
                        }
2369 113
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2370 113
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2371 113
                    }
2372 187
                    $this->doPersist($relatedDocument, $visited);
2373 339
                }
2374 390
            } elseif ($relatedDocuments !== null) {
2375 120
                if ( ! empty($mapping['embedded'])) {
2376 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2377 66
                    if ($knownParent && $knownParent !== $document) {
2378 5
                        $relatedDocuments = clone $relatedDocuments;
2379 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2380 5
                    }
2381 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2382 66
                }
2383 120
                $this->doPersist($relatedDocuments, $visited);
2384 119
            }
2385 568
        }
2386 567
    }
2387
2388
    /**
2389
     * Cascades the delete operation to associated documents.
2390
     *
2391
     * @param object $document
2392
     * @param array $visited
2393
     */
2394 66 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...
2395
    {
2396 66
        $class = $this->dm->getClassMetadata(get_class($document));
2397 66
        foreach ($class->fieldMappings as $mapping) {
2398 66
            if ( ! $mapping['isCascadeRemove']) {
2399 66
                continue;
2400
            }
2401 33
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2402 2
                $document->__load();
2403 2
            }
2404
2405 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2406 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2407
                // If its a PersistentCollection initialization is intended! No unwrap!
2408 24
                foreach ($relatedDocuments as $relatedDocument) {
2409 13
                    $this->doRemove($relatedDocument, $visited);
2410 24
                }
2411 33
            } elseif ($relatedDocuments !== null) {
2412 12
                $this->doRemove($relatedDocuments, $visited);
2413 12
            }
2414 66
        }
2415 66
    }
2416
2417
    /**
2418
     * Acquire a lock on the given document.
2419
     *
2420
     * @param object $document
2421
     * @param int $lockMode
2422
     * @param int $lockVersion
2423
     * @throws LockException
2424
     * @throws \InvalidArgumentException
2425
     */
2426 9
    public function lock($document, $lockMode, $lockVersion = null)
2427
    {
2428 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2429 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2430
        }
2431
2432 8
        $documentName = get_class($document);
2433 8
        $class = $this->dm->getClassMetadata($documentName);
2434
2435 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2436 3
            if ( ! $class->isVersioned) {
2437 1
                throw LockException::notVersioned($documentName);
2438
            }
2439
2440 2
            if ($lockVersion != null) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $lockVersion of type integer|null against null; this is ambiguous if the integer can be zero. Consider using a strict comparison !== instead.
Loading history...
2441 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2442 2
                if ($documentVersion != $lockVersion) {
2443 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2444
                }
2445 1
            }
2446 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2447 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2448 5
        }
2449 6
    }
2450
2451
    /**
2452
     * Releases a lock on the given document.
2453
     *
2454
     * @param object $document
2455
     * @throws \InvalidArgumentException
2456
     */
2457 1
    public function unlock($document)
2458
    {
2459 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2460
            throw new \InvalidArgumentException("Document is not MANAGED.");
2461
        }
2462 1
        $documentName = get_class($document);
2463 1
        $this->getDocumentPersister($documentName)->unlock($document);
2464 1
    }
2465
2466
    /**
2467
     * Clears the UnitOfWork.
2468
     *
2469
     * @param string|null $documentName if given, only documents of this type will get detached.
2470
     */
2471 389
    public function clear($documentName = null)
2472
    {
2473 389
        if ($documentName === null) {
2474 383
            $this->identityMap =
2475 383
            $this->documentIdentifiers =
2476 383
            $this->originalDocumentData =
2477 383
            $this->documentChangeSets =
2478 383
            $this->documentStates =
2479 383
            $this->scheduledForDirtyCheck =
2480 383
            $this->documentInsertions =
2481 383
            $this->documentUpserts =
2482 383
            $this->documentUpdates =
2483 383
            $this->documentDeletions =
2484 383
            $this->collectionUpdates =
2485 383
            $this->collectionDeletions =
2486 383
            $this->parentAssociations =
2487 383
            $this->orphanRemovals = 
2488 383
            $this->hasScheduledCollections = array();
2489 383
        } else {
2490 6
            $visited = array();
2491 6
            foreach ($this->identityMap as $className => $documents) {
2492 6
                if ($className === $documentName) {
2493 3
                    foreach ($documents as $document) {
2494 3
                        $this->doDetach($document, $visited);
2495 3
                    }
2496 3
                }
2497 6
            }
2498
        }
2499
2500 389 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...
2501
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2502
        }
2503 389
    }
2504
2505
    /**
2506
     * INTERNAL:
2507
     * Schedules an embedded document for removal. The remove() operation will be
2508
     * invoked on that document at the beginning of the next commit of this
2509
     * UnitOfWork.
2510
     *
2511
     * @ignore
2512
     * @param object $document
2513
     */
2514 47
    public function scheduleOrphanRemoval($document)
2515
    {
2516 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2517 47
    }
2518
2519
    /**
2520
     * INTERNAL:
2521
     * Unschedules an embedded or referenced object for removal.
2522
     *
2523
     * @ignore
2524
     * @param object $document
2525
     */
2526 103
    public function unscheduleOrphanRemoval($document)
2527
    {
2528 103
        $oid = spl_object_hash($document);
2529 103
        if (isset($this->orphanRemovals[$oid])) {
2530 1
            unset($this->orphanRemovals[$oid]);
2531 1
        }
2532 103
    }
2533
2534
    /**
2535
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2536
     *  1) sets owner if it was cloned
2537
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2538
     *  3) NOP if state is OK
2539
     * Returned collection should be used from now on (only important with 2nd point)
2540
     *
2541
     * @param PersistentCollection $coll
2542
     * @param object $document
2543
     * @param ClassMetadata $class
2544
     * @param string $propName
2545
     * @return PersistentCollection
2546
     */
2547 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2548
    {
2549 8
        $owner = $coll->getOwner();
2550 8
        if ($owner === null) { // cloned
2551 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2552 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2553 2
            if ( ! $coll->isInitialized()) {
2554 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2555 1
            }
2556 2
            $newValue = clone $coll;
2557 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2558 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2559 2
            if ($this->isScheduledForUpdate($document)) {
2560
                // @todo following line should be superfluous once collections are stored in change sets
2561
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2562
            }
2563 2
            return $newValue;
2564
        }
2565 6
        return $coll;
2566
    }
2567
2568
    /**
2569
     * INTERNAL:
2570
     * Schedules a complete collection for removal when this UnitOfWork commits.
2571
     *
2572
     * @param PersistentCollection $coll
2573
     */
2574 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2575
    {
2576 41
        $oid = spl_object_hash($coll);
2577 41
        unset($this->collectionUpdates[$oid]);
2578 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2579 41
            $this->collectionDeletions[$oid] = $coll;
2580 41
            $this->scheduleCollectionOwner($coll);
2581 41
        }
2582 41
    }
2583
2584
    /**
2585
     * Checks whether a PersistentCollection is scheduled for deletion.
2586
     *
2587
     * @param PersistentCollection $coll
2588
     * @return boolean
2589
     */
2590 106
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2591
    {
2592 106
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2593
    }
2594
    
2595
    /**
2596
     * INTERNAL:
2597
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2598
     * 
2599
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2600
     */
2601 206 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2602
    {
2603 206
        $oid = spl_object_hash($coll);
2604 206
        if (isset($this->collectionDeletions[$oid])) {
2605 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2606 11
            unset($this->collectionDeletions[$oid]);
2607 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2608 11
        }
2609 206
    }
2610
2611
    /**
2612
     * INTERNAL:
2613
     * Schedules a collection for update when this UnitOfWork commits.
2614
     *
2615
     * @param PersistentCollection $coll
2616
     */
2617 222
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2618
    {
2619 222
        $mapping = $coll->getMapping();
2620 222
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2621
            /* There is no need to $unset collection if it will be $set later
2622
             * This is NOP if collection is not scheduled for deletion
2623
             */
2624 40
            $this->unscheduleCollectionDeletion($coll);
2625 40
        }
2626 222
        $oid = spl_object_hash($coll);
2627 222
        if ( ! isset($this->collectionUpdates[$oid])) {
2628 222
            $this->collectionUpdates[$oid] = $coll;
2629 222
            $this->scheduleCollectionOwner($coll);
2630 222
        }
2631 222
    }
2632
    
2633
    /**
2634
     * INTERNAL:
2635
     * Unschedules a collection from being updated when this UnitOfWork commits.
2636
     * 
2637
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2638
     */
2639 206 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2640
    {
2641 206
        $oid = spl_object_hash($coll);
2642 206
        if (isset($this->collectionUpdates[$oid])) {
2643 196
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2644 196
            unset($this->collectionUpdates[$oid]);
2645 196
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2646 196
        }
2647 206
    }
2648
    
2649
    /**
2650
     * Checks whether a PersistentCollection is scheduled for update.
2651
     *
2652
     * @param PersistentCollection $coll
2653
     * @return boolean
2654
     */
2655 122
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2656
    {
2657 122
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2658
    }
2659
2660
    /**
2661
     * INTERNAL:
2662
     * Gets PersistentCollections that have been visited during computing change
2663
     * set of $document
2664
     *
2665
     * @param object $document
2666
     * @return PersistentCollection[]
2667
     */
2668 537
    public function getVisitedCollections($document)
2669
    {
2670 537
        $oid = spl_object_hash($document);
2671 537
        return isset($this->visitedCollections[$oid])
2672 537
                ? $this->visitedCollections[$oid]
2673 537
                : array();
2674
    }
2675
    
2676
    /**
2677
     * INTERNAL:
2678
     * Gets PersistentCollections that are scheduled to update and related to $document
2679
     * 
2680
     * @param object $document
2681
     * @return array
2682
     */
2683 537
    public function getScheduledCollections($document)
2684
    {
2685 537
        $oid = spl_object_hash($document);
2686 537
        return isset($this->hasScheduledCollections[$oid]) 
2687 537
                ? $this->hasScheduledCollections[$oid]
2688 537
                : array();
2689
    }
2690
    
2691
    /**
2692
     * Checks whether the document is related to a PersistentCollection
2693
     * scheduled for update or deletion.
2694
     *
2695
     * @param object $document
2696
     * @return boolean
2697
     */
2698 60
    public function hasScheduledCollections($document)
2699
    {
2700 60
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2701
    }
2702
    
2703
    /**
2704
     * Marks the PersistentCollection's top-level owner as having a relation to
2705
     * a collection scheduled for update or deletion.
2706
     *
2707
     * If the owner is not scheduled for any lifecycle action, it will be
2708
     * scheduled for update to ensure that versioning takes place if necessary.
2709
     *
2710
     * If the collection is nested within atomic collection, it is immediately
2711
     * unscheduled and atomic one is scheduled for update instead. This makes
2712
     * calculating update data way easier.
2713
     * 
2714
     * @param PersistentCollection $coll
2715
     */
2716 224
    private function scheduleCollectionOwner(PersistentCollection $coll)
2717
    {
2718 224
        $document = $this->getOwningDocument($coll->getOwner());
2719 224
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2720
2721 224
        if ($document !== $coll->getOwner()) {
2722 24
            $parent = $coll->getOwner();
2723 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2724 24
                list($mapping, $parent, ) = $parentAssoc;
2725 24
            }
2726 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2727 7
                $class = $this->dm->getClassMetadata(get_class($document));
2728 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2729 7
                $this->scheduleCollectionUpdate($atomicCollection);
2730 7
                $this->unscheduleCollectionDeletion($coll);
2731 7
                $this->unscheduleCollectionUpdate($coll);
2732 7
            }
2733 24
        }
2734
2735 224
        if ( ! $this->isDocumentScheduled($document)) {
2736 92
            $this->scheduleForUpdate($document);
2737 92
        }
2738 224
    }
2739
2740
    /**
2741
     * Get the top-most owning document of a given document
2742
     *
2743
     * If a top-level document is provided, that same document will be returned.
2744
     * For an embedded document, we will walk through parent associations until
2745
     * we find a top-level document.
2746
     *
2747
     * @param object $document
2748
     * @throws \UnexpectedValueException when a top-level document could not be found
2749
     * @return object
2750
     */
2751 226
    public function getOwningDocument($document)
2752
    {
2753 226
        $class = $this->dm->getClassMetadata(get_class($document));
2754 226
        while ($class->isEmbeddedDocument) {
2755 38
            $parentAssociation = $this->getParentAssociation($document);
2756
2757 38
            if ( ! $parentAssociation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentAssociation of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2758
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2759
            }
2760
2761 38
            list(, $document, ) = $parentAssociation;
2762 38
            $class = $this->dm->getClassMetadata(get_class($document));
2763 38
        }
2764
2765 226
        return $document;
2766
    }
2767
2768
    /**
2769
     * Gets the class name for an association (embed or reference) with respect
2770
     * to any discriminator value.
2771
     *
2772
     * @param array      $mapping Field mapping for the association
2773
     * @param array|null $data    Data for the embedded document or reference
2774
     */
2775 207
    public function getClassNameForAssociation(array $mapping, $data)
2776
    {
2777 207
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2778
2779 207
        $discriminatorValue = null;
2780 207
        if (isset($discriminatorField, $data[$discriminatorField])) {
2781 21
            $discriminatorValue = $data[$discriminatorField];
2782 207
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2783
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2784
        }
2785
2786 207
        if ($discriminatorValue !== null) {
2787 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2788 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2789 21
                : $discriminatorValue;
2790
        }
2791
2792 187
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2793
2794 187 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2795 15
            $discriminatorValue = $data[$class->discriminatorField];
2796 187
        } elseif ($class->defaultDiscriminatorValue !== null) {
2797 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2798 1
        }
2799
2800 187
        if ($discriminatorValue !== null) {
2801 16
            return isset($class->discriminatorMap[$discriminatorValue])
2802 16
                ? $class->discriminatorMap[$discriminatorValue]
2803 16
                : $discriminatorValue;
2804
        }
2805
2806 171
        return $mapping['targetDocument'];
2807
    }
2808
2809
    /**
2810
     * INTERNAL:
2811
     * Creates a document. Used for reconstitution of documents during hydration.
2812
     *
2813
     * @ignore
2814
     * @param string $className The name of the document class.
2815
     * @param array $data The data for the document.
2816
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2817
     * @param object The document to be hydrated into in case of creation
2818
     * @return object The document instance.
2819
     * @internal Highly performance-sensitive method.
2820
     */
2821 382
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2822
    {
2823 382
        $class = $this->dm->getClassMetadata($className);
2824
2825
        // @TODO figure out how to remove this
2826 382
        $discriminatorValue = null;
2827 382 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...
2828 17
            $discriminatorValue = $data[$class->discriminatorField];
2829 382
        } elseif (isset($class->defaultDiscriminatorValue)) {
2830 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2831 2
        }
2832
2833 382
        if ($discriminatorValue !== null) {
2834 18
            $className = isset($class->discriminatorMap[$discriminatorValue])
2835 18
                ? $class->discriminatorMap[$discriminatorValue]
2836 18
                : $discriminatorValue;
2837
2838 18
            $class = $this->dm->getClassMetadata($className);
2839
2840 18
            unset($data[$class->discriminatorField]);
2841 18
        }
2842
2843 382
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2844 382
        $serializedId = serialize($id);
2845
2846 382
        if (isset($this->identityMap[$class->name][$serializedId])) {
2847 89
            $document = $this->identityMap[$class->name][$serializedId];
2848 89
            $oid = spl_object_hash($document);
2849 89
            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...
2850 10
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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