Completed
Push — 1.0.x ( 17be5e...3a014b )
by Maciej
08:48
created

UnitOfWork::scheduleForInsert()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.3074
Metric Value
dl 0
loc 20
ccs 10
cts 13
cp 0.7692
rs 8.8571
cc 5
eloc 11
nc 5
nop 2
crap 5.3074
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 920
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
260
    {
261 920
        $this->dm = $dm;
262 920
        $this->evm = $evm;
263 920
        $this->hydratorFactory = $hydratorFactory;
264 920
    }
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 667
    public function getPersistenceBuilder()
273
    {
274 667
        if ( ! $this->persistenceBuilder) {
275 667
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
276 667
        }
277 667
        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 665
    public function getDocumentPersister($documentName)
320
    {
321 665
        if ( ! isset($this->persisters[$documentName])) {
322 651
            $class = $this->dm->getClassMetadata($documentName);
323 651
            $pb = $this->getPersistenceBuilder();
324 651
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this->evm, $this, $this->hydratorFactory, $class);
325 651
        }
326 665
        return $this->persisters[$documentName];
327
    }
328
329
    /**
330
     * Get the collection persister instance.
331
     *
332
     * @return \Doctrine\ODM\MongoDB\Persisters\CollectionPersister
333
     */
334 665
    public function getCollectionPersister()
335
    {
336 665
        if ( ! isset($this->collectionPersister)) {
337 665
            $pb = $this->getPersistenceBuilder();
338 665
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
339 665
        }
340 665
        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 554
    public function commit($document = null, array $options = array())
369
    {
370
        // Raise preFlush
371 554
        if ($this->evm->hasListeners(Events::preFlush)) {
372
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
373
        }
374
375 554
        $defaultOptions = $this->dm->getConfiguration()->getDefaultCommitOptions();
376 554
        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 554
            $options = $defaultOptions;
380
        }
381
        // Compute changes done since last commit.
382 554
        if ($document === null) {
383 548
            $this->computeChangeSets();
384 553
        } 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 552
        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 235
            $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 198
            $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 188
            $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 552
        ) {
400 23
            return; // Nothing to do.
401
        }
402
403 549
        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 549
        if ($this->evm->hasListeners(Events::onFlush)) {
411 7
            $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
412 7
        }
413
414 549
        foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
415 78
            list($class, $documents) = $classAndDocuments;
416 78
            $this->executeUpserts($class, $documents, $options);
417 549
        }
418
419 549
        foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
420 482
            list($class, $documents) = $classAndDocuments;
421 482
            $this->executeInserts($class, $documents, $options);
422 548
        }
423
424 548
        foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
425 214
            list($class, $documents) = $classAndDocuments;
426 214
            $this->executeUpdates($class, $documents, $options);
427 548
        }
428
429 548
        foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
430 62
            list($class, $documents) = $classAndDocuments;
431 62
            $this->executeDeletions($class, $documents, $options);
432 548
        }
433
434
        // Raise postFlush
435 548
        if ($this->evm->hasListeners(Events::postFlush)) {
436
            $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
437
        }
438
439
        // Clear up
440 548
        $this->documentInsertions =
441 548
        $this->documentUpserts =
442 548
        $this->documentUpdates =
443 548
        $this->documentDeletions =
444 548
        $this->documentChangeSets =
445 548
        $this->collectionUpdates =
446 548
        $this->collectionDeletions =
447 548
        $this->visitedCollections =
448 548
        $this->scheduledForDirtyCheck =
449 548
        $this->orphanRemovals = 
450 548
        $this->hasScheduledCollections = array();
451 548
    }
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 549
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
461
    {
462 549
        if (empty($documents)) {
463 549
            return array();
464
        }
465 548
        $divided = array();
466 548
        $embeds = array();
467 548
        foreach ($documents as $oid => $d) {
468 548
            $className = get_class($d);
469 548
            if (isset($embeds[$className])) {
470 68
                continue;
471
            }
472 548
            if (isset($divided[$className])) {
473 135
                $divided[$className][1][$oid] = $d;
474 135
                continue;
475
            }
476 548
            $class = $this->dm->getClassMetadata($className);
477 548
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
478 165
                $embeds[$className] = true;
479 165
                continue;
480
            }
481 548
            if (empty($divided[$class->name])) {
482 548
                $divided[$class->name] = array($class, array($oid => $d));
483 548
            } else {
484 4
                $divided[$class->name][1][$oid] = $d;
485
            }
486 548
        }
487 548
        return $divided;
488
    }
489
490
    /**
491
     * Compute changesets of all documents scheduled for insertion.
492
     *
493
     * Embedded documents will not be processed.
494
     */
495 556 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 556
        foreach ($this->documentInsertions as $document) {
498 490
            $class = $this->dm->getClassMetadata(get_class($document));
499 490
            if ( ! $class->isEmbeddedDocument) {
500 487
                $this->computeChangeSet($class, $document);
501 486
            }
502 555
        }
503 555
    }
504
505
    /**
506
     * Compute changesets of all documents scheduled for upsert.
507
     *
508
     * Embedded documents will not be processed.
509
     */
510 555 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 555
        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 555
        }
518 555
    }
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 539
    public function getDocumentChangeSet($document)
573
    {
574 539
        $oid = spl_object_hash($document);
575 539
        if (isset($this->documentChangeSets[$oid])) {
576 539
            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 553
    public function getDocumentActualData($document)
588
    {
589 553
        $class = $this->dm->getClassMetadata(get_class($document));
590 553
        $actualData = array();
591 553
        foreach ($class->reflFields as $name => $refProp) {
592 553
            $mapping = $class->fieldMappings[$name];
593
            // skip not saved fields
594 553
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
595 49
                continue;
596
            }
597 553
            $value = $refProp->getValue($document);
598 553
            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 553
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
603 553
                && $value !== null && ! ($value instanceof PersistentCollection)) {
604
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
605 365
                if ( ! $value instanceof Collection) {
606 118
                    $value = new ArrayCollection($value);
607 118
                }
608
609
                // Inject PersistentCollection
610 365
                $coll = new PersistentCollection($value, $this->dm, $this);
611 365
                $coll->setOwner($document, $mapping);
612 365
                $coll->setDirty( ! $value->isEmpty());
613 365
                $class->reflFields[$name]->setValue($document, $coll);
614 365
                $actualData[$name] = $coll;
615 365
            } else {
616 553
                $actualData[$name] = $value;
617
            }
618 553
        }
619 553
        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 553
    public function computeChangeSet(ClassMetadata $class, $document)
647
    {
648 553
        if ( ! $class->isInheritanceTypeNone()) {
649 171
            $class = $this->dm->getClassMetadata(get_class($document));
650 171
        }
651
652
        // Fire PreFlush lifecycle callbacks
653 553
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
654 10
            $class->invokeLifecycleCallbacks(Events::preFlush, $document);
655 10
        }
656
657 553
        $this->computeOrRecomputeChangeSet($class, $document);
658 552
    }
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 553
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
668
    {
669 553
        $oid = spl_object_hash($document);
670 553
        $actualData = $this->getDocumentActualData($document);
671 553
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
672 553
        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 553
            $this->originalDocumentData[$oid] = $actualData;
676 553
            $changeSet = array();
677 553
            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 553
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
682
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
683
                    $actualValue = $actualData[$propName];
684
                }
685 553
                $changeSet[$propName] = array(null, $actualValue);
686 553
            }
687 553
            $this->documentChangeSets[$oid] = $changeSet;
688 553
        } else {
689
            // Document is "fully" MANAGED: it was already fully persisted before
690
            // and we have a copy of the original data
691 274
            $originalData = $this->originalDocumentData[$oid];
692 274
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
693 274
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
694 2
                $changeSet = $this->documentChangeSets[$oid];
695 2
            } else {
696 274
                $changeSet = array();
697
            }
698
699 274
            foreach ($actualData as $propName => $actualValue) {
700
                // skip not saved fields
701 274
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
702
                    continue;
703
                }
704
705 274
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
706
707
                // skip if value has not changed
708 274
                if ($orgValue === $actualValue) {
709
                    // but consider dirty GridFSFile instances as changed
710 273
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
711 273
                        continue;
712
                    }
713 1
                }
714
715
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
716 175
                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 168
                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 160
                if ($isChangeTrackingNotify) {
736 2
                    continue;
737
                }
738
739
                // ignore inverse side of reference-many relationship
740 159
                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 159
                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 159
                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 145
                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 129
                $changeSet[$propName] = array($orgValue, $actualValue);
779 274
            }
780 274
            if ($changeSet) {
781 161
                $this->documentChangeSets[$oid] = ($recompute && isset($this->documentChangeSets[$oid]))
782 161
                    ? $changeSet + $this->documentChangeSets[$oid]
783 12
                    : $changeSet;
784
785 161
                $this->originalDocumentData[$oid] = $actualData;
786 161
                $this->scheduleForUpdate($document);
787 161
            }
788
        }
789
790
        // Look for changes in associations of the document
791 553
        $associationMappings = array_filter(
792 553
            $class->associationMappings,
793
            function ($assoc) { return empty($assoc['notSaved']); }
794 553
        );
795
796 553
        foreach ($associationMappings as $mapping) {
797 427
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
798
799 427
            if ($value === null) {
800 287
                continue;
801
            }
802
803 418
            $this->computeAssociationChanges($document, $mapping, $value);
804
805 417
            if (isset($mapping['reference'])) {
806 314
                continue;
807
            }
808
809 326
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
810
811 326
            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 326
            }
824 552
        }
825 552
    }
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 551
    public function computeChangeSets()
833
    {
834 551
        $this->computeScheduleInsertsChangeSets();
835 550
        $this->computeScheduleUpsertsChangeSets();
836
837
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
838 550
        foreach ($this->identityMap as $className => $documents) {
839 550
            $class = $this->dm->getClassMetadata($className);
840 550
            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 550
            switch (true) {
852 550
                case ($class->isChangeTrackingDeferredImplicit()):
853 549
                    $documentsToProcess = $documents;
854 549
                    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 550
            foreach ($documentsToProcess as $document) {
866
                // Ignore uninitialized proxy objects
867 546
                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 546
                $oid = spl_object_hash($document);
872 546 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 546
                    && ! isset($this->documentUpserts[$oid])
874 546
                    && ! isset($this->documentDeletions[$oid])
875 546
                    && isset($this->documentStates[$oid])
876 546
                ) {
877 259
                    $this->computeChangeSet($class, $document);
878 259
                }
879 550
            }
880 550
        }
881 550
    }
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 418
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
892
    {
893 418
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
894 418
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
895 418
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
896
897 418
        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 417
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
902 225
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
903 221
                $this->scheduleCollectionUpdate($value);
904 221
            }
905 225
            $topmostOwner = $this->getOwningDocument($value->getOwner());
906 225
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
907 225
            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 225
        }
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 417
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
919
920 417
        $count = 0;
921 417
        foreach ($unwrappedValue as $key => $entry) {
922 322
            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 321
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
929
930 321
            $state = $this->getDocumentState($entry, self::STATE_NEW);
931
932
            // Handle "set" strategy for multi-level hierarchy
933 321
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
934 321
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
935
936 321
            $count++;
937
938
            switch ($state) {
939 321
                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 317
                case self::STATE_MANAGED:
953 317
                    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 317
                    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
                    break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
985
986
                default:
987
                    // MANAGED associated documents are already taken into account
988
                    // during changeset calculation anyway, since they are in the identity map.
989
990
            }
991 416
        }
992 416
    }
993
994
    /**
995
     * INTERNAL:
996
     * Computes the changeset of an individual document, independently of the
997
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
998
     *
999
     * The passed document must be a managed document. If the document already has a change set
1000
     * because this method is invoked during a commit cycle then the change sets are added.
1001
     * whereby changes detected in this method prevail.
1002
     *
1003
     * @ignore
1004
     * @param ClassMetadata $class The class descriptor of the document.
1005
     * @param object $document The document for which to (re)calculate the change set.
1006
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1007
     */
1008 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1009
    {
1010
        // Ignore uninitialized proxy objects
1011 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...
1012 1
            return;
1013
        }
1014
1015 18
        $oid = spl_object_hash($document);
1016
1017 18
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1018
            throw new \InvalidArgumentException('Document must be managed.');
1019
        }
1020
1021 18
        if ( ! $class->isInheritanceTypeNone()) {
1022 2
            $class = $this->dm->getClassMetadata(get_class($document));
1023 2
        }
1024
1025 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1026 18
    }
1027
1028
    /**
1029
     * @param ClassMetadata $class
1030
     * @param object $document
1031
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1032
     */
1033 571
    private function persistNew(ClassMetadata $class, $document)
1034
    {
1035 571
        $oid = spl_object_hash($document);
1036 571
        if ( ! empty($class->lifecycleCallbacks[Events::prePersist])) {
1037 155
            $class->invokeLifecycleCallbacks(Events::prePersist, $document);
1038 155
        }
1039 571 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...
1040 6
            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($document, $this->dm));
1041 6
        }
1042
1043 571
        $upsert = false;
1044 571
        if ($class->identifier) {
1045 571
            $idValue = $class->getIdentifierValue($document);
1046 571
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1047
1048 571
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1049 3
                throw new \InvalidArgumentException(sprintf(
1050 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1051 3
                    get_class($document)
1052 3
                ));
1053
            }
1054
1055
            // \MongoId::isValid($idValue) was introduced in 1.5.0 so it's no good
1056 570
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('/^[0-9a-f]{24}$/', $idValue)) {
1057 1
                throw new \InvalidArgumentException(sprintf(
1058 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1059 1
                    get_class($document)
1060 1
                ));
1061
            }
1062
1063 569
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1064 498
                $idValue = $class->idGenerator->generate($this->dm, $document);
1065 498
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1066 498
                $class->setIdentifierValue($document, $idValue);
1067 498
            }
1068
1069 569
            $this->documentIdentifiers[$oid] = $idValue;
1070 569
        } else {
1071
            // this is for embedded documents without identifiers
1072 142
            $this->documentIdentifiers[$oid] = $oid;
1073
        }
1074
1075 569
        $this->documentStates[$oid] = self::STATE_MANAGED;
1076
1077 569
        if ($upsert) {
1078 81
            $this->scheduleForUpsert($class, $document);
1079 81
        } else {
1080 503
            $this->scheduleForInsert($class, $document);
1081
        }
1082 569
    }
1083
1084
    /**
1085
     * Cascades the postPersist events to embedded documents.
1086
     *
1087
     * @param ClassMetadata $class
1088
     * @param object $document
1089
     */
1090 547
    private function cascadePostPersist(ClassMetadata $class, $document)
1091
    {
1092 547
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1093
1094 547
        $embeddedMappings = array_filter(
1095 547
            $class->associationMappings,
1096
            function($assoc) { return ! empty($assoc['embedded']); }
1097 547
        );
1098
1099 547
        foreach ($embeddedMappings as $mapping) {
1100 334
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1101
1102 334
            if ($value === null) {
1103 213
                continue;
1104
            }
1105
1106 316
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1107
1108 316
            if (isset($mapping['targetDocument'])) {
1109 305
                $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1110 305
            }
1111
1112 316
            foreach ($values as $embeddedDocument) {
1113 159
                if ( ! isset($mapping['targetDocument'])) {
1114 13
                    $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument));
1115 13
                }
1116
1117 159
                if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) {
1118 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...
1119 9
                }
1120 159
                if ($hasPostPersistListeners) {
1121 4
                    $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm));
1122 4
                }
1123 159
                $this->cascadePostPersist($embeddedClass, $embeddedDocument);
1124 316
            }
1125 547
         }
1126 547
     }
1127
1128
    /**
1129
     * Executes all document insertions for documents of the specified type.
1130
     *
1131
     * @param ClassMetadata $class
1132
     * @param array $documents Array of documents to insert
1133
     * @param array $options Array of options to be used with batchInsert()
1134
     */
1135 482 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...
1136
    {
1137 482
        $persister = $this->getDocumentPersister($class->name);
1138
1139 482
        foreach ($documents as $oid => $document) {
1140 482
            $persister->addInsert($document);
1141 482
            unset($this->documentInsertions[$oid]);
1142 482
        }
1143
1144 482
        $persister->executeInserts($options);
1145
1146 481
        $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]);
1147 481
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1148
1149 481
        foreach ($documents as $document) {
1150 481
            if ($hasPostPersistLifecycleCallbacks) {
1151 9
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1152 9
            }
1153 481
            if ($hasPostPersistListeners) {
1154 5
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1155 5
            }
1156 481
            $this->cascadePostPersist($class, $document);
1157 481
        }
1158 481
    }
1159
1160
    /**
1161
     * Executes all document upserts for documents of the specified type.
1162
     *
1163
     * @param ClassMetadata $class
1164
     * @param array $documents Array of documents to upsert
1165
     * @param array $options Array of options to be used with batchInsert()
1166
     */
1167 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...
1168
    {
1169 78
        $persister = $this->getDocumentPersister($class->name);
1170
1171
1172 78
        foreach ($documents as $oid => $document) {
1173 78
            $persister->addUpsert($document);
1174 78
            unset($this->documentUpserts[$oid]);
1175 78
        }
1176
1177 78
        $persister->executeUpserts($options);
1178
1179 78
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
1180 78
        $hasListeners = $this->evm->hasListeners(Events::postPersist);
1181
1182 78
        foreach ($documents as $document) {
1183 78
            if ($hasLifecycleCallbacks) {
1184
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1185
            }
1186 78
            if ($hasListeners) {
1187 2
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1188 2
            }
1189 78
            $this->cascadePostPersist($class, $document);
1190 78
        }
1191 78
    }
1192
1193
    /**
1194
     * Executes all document updates for documents of the specified type.
1195
     *
1196
     * @param Mapping\ClassMetadata $class
1197
     * @param array $documents Array of documents to update
1198
     * @param array $options Array of options to be used with update()
1199
     */
1200 214
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1201
    {
1202 214
        $className = $class->name;
1203 214
        $persister = $this->getDocumentPersister($className);
1204
1205 214
        $hasPreUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::preUpdate]);
1206 214
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1207 214
        $hasPostUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postUpdate]);
1208 214
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1209
1210 214
        foreach ($documents as $oid => $document) {
1211 214
            if ($hasPreUpdateLifecycleCallbacks) {
1212 11
                $class->invokeLifecycleCallbacks(Events::preUpdate, $document);
1213 11
                $this->recomputeSingleDocumentChangeSet($class, $document);
1214 11
            }
1215
1216 214
            if ($hasPreUpdateListeners) {
1217 8
                if ( ! isset($this->documentChangeSets[$oid])) {
1218
                    // only ReferenceMany collection is scheduled for update
1219 1
                    $this->documentChangeSets[$oid] = array();
1220 1
                }
1221 8
                $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1222 8
                    $document, $this->dm, $this->documentChangeSets[$oid])
1223 8
                );
1224 8
            }
1225 214
            $this->cascadePreUpdate($class, $document);
1226
1227 214
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1228 213
                $persister->update($document, $options);
1229 209
            }
1230
1231 210
            unset($this->documentUpdates[$oid]);
1232
1233 210
            if ($hasPostUpdateLifecycleCallbacks) {
1234 6
                $class->invokeLifecycleCallbacks(Events::postUpdate, $document);
1235 6
            }
1236 210
            if ($hasPostUpdateListeners) {
1237 8
                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm));
1238 8
            }
1239 210
            $this->cascadePostUpdate($class, $document);
1240 210
        }
1241 209
    }
1242
1243
    /**
1244
     * Cascades the preUpdate event to embedded documents.
1245
     *
1246
     * @param ClassMetadata $class
1247
     * @param object $document
1248
     */
1249 214
    private function cascadePreUpdate(ClassMetadata $class, $document)
1250
    {
1251 214
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1252
1253 214
        $embeddedMappings = array_filter(
1254 214
            $class->associationMappings,
1255
            function ($assoc) { return ! empty($assoc['embedded']); }
1256 214
        );
1257
1258 214
        foreach ($embeddedMappings as $mapping) {
1259 133
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1260
1261 133
            if ($value === null) {
1262 49
                continue;
1263
            }
1264
1265 131
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1266
1267 131
            foreach ($values as $entry) {
1268 84
                $entryOid = spl_object_hash($entry);
1269 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1270
1271 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1272 47
                    continue;
1273
                }
1274
1275 67
                if (isset($this->documentInsertions[$entryOid])) {
1276 52
                    continue;
1277
                }
1278
1279 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...
1280 5
                    $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry);
1281 5
                    $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
1282 5
                }
1283 45
                if ($hasPreUpdateListeners) {
1284 3
                    $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1285 3
                        $entry, $this->dm, $this->documentChangeSets[$entryOid])
1286 3
                    );
1287 3
                }
1288
1289 45
                $this->cascadePreUpdate($entryClass, $entry);
1290 131
            }
1291 214
        }
1292 214
    }
1293
1294
    /**
1295
     * Cascades the postUpdate and postPersist events to embedded documents.
1296
     *
1297
     * @param ClassMetadata $class
1298
     * @param object $document
1299
     */
1300 210
    private function cascadePostUpdate(ClassMetadata $class, $document)
1301
    {
1302 210
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1303 210
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1304
1305 210
        $embeddedMappings = array_filter(
1306 210
            $class->associationMappings,
1307
            function($assoc) { return ! empty($assoc['embedded']); }
1308 210
        );
1309
1310 210
        foreach ($embeddedMappings as $mapping) {
1311 129
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1312
1313 129
            if ($value === null) {
1314 52
                continue;
1315
            }
1316
1317 127
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1318
1319 127
            foreach ($values as $entry) {
1320 84
                $entryOid = spl_object_hash($entry);
1321 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1322
1323 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1324 47
                    continue;
1325
                }
1326
1327 67
                if (isset($this->documentInsertions[$entryOid])) {
1328 52
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) {
1329 1
                        $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry);
1330 1
                    }
1331 52
                    if ($hasPostPersistListeners) {
1332 3
                        $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm));
1333 3
                    }
1334 52
                } else {
1335 45
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) {
1336 9
                        $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry);
1337 9
                    }
1338 45
                    if ($hasPostUpdateListeners) {
1339 3
                        $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm));
1340 3
                    }
1341
                }
1342
1343 67
                $this->cascadePostUpdate($entryClass, $entry);
1344 127
            }
1345 210
        }
1346 210
    }
1347
1348
    /**
1349
     * Executes all document deletions for documents of the specified type.
1350
     *
1351
     * @param ClassMetadata $class
1352
     * @param array $documents Array of documents to delete
1353
     * @param array $options Array of options to be used with remove()
1354
     */
1355 62
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1356
    {
1357 62
        $hasPostRemoveLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postRemove]);
1358 62
        $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove);
1359
1360 62
        $persister = $this->getDocumentPersister($class->name);
1361
1362 62
        foreach ($documents as $oid => $document) {
1363 62
            if ( ! $class->isEmbeddedDocument) {
1364 28
                $persister->delete($document, $options);
1365 26
            }
1366
            unset(
1367 60
                $this->documentDeletions[$oid],
1368 60
                $this->documentIdentifiers[$oid],
1369 60
                $this->originalDocumentData[$oid]
1370
            );
1371
1372
            // Clear snapshot information for any referenced PersistentCollection
1373
            // http://www.doctrine-project.org/jira/browse/MODM-95
1374 60
            foreach ($class->associationMappings as $fieldMapping) {
1375 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1376 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1377 26
                    if ($value instanceof PersistentCollection) {
1378 22
                        $value->clearSnapshot();
1379 22
                    }
1380 26
                }
1381 60
            }
1382
1383
            // Document with this $oid after deletion treated as NEW, even if the $oid
1384
            // is obtained by a new document because the old one went out of scope.
1385 60
            $this->documentStates[$oid] = self::STATE_NEW;
1386
1387 60
            if ($hasPostRemoveLifecycleCallbacks) {
1388 8
                $class->invokeLifecycleCallbacks(Events::postRemove, $document);
1389 8
            }
1390 60
            if ($hasPostRemoveListeners) {
1391 2
                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm));
1392 2
            }
1393 60
        }
1394 60
    }
1395
1396
    /**
1397
     * Schedules a document for insertion into the database.
1398
     * If the document already has an identifier, it will be added to the
1399
     * identity map.
1400
     *
1401
     * @param ClassMetadata $class
1402
     * @param object $document The document to schedule for insertion.
1403
     * @throws \InvalidArgumentException
1404
     */
1405 506
    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...
1406
    {
1407 506
        $oid = spl_object_hash($document);
1408
1409 506
        if (isset($this->documentUpdates[$oid])) {
1410
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1411
        }
1412 506
        if (isset($this->documentDeletions[$oid])) {
1413
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1414
        }
1415 506
        if (isset($this->documentInsertions[$oid])) {
1416
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1417
        }
1418
1419 506
        $this->documentInsertions[$oid] = $document;
1420
1421 506
        if (isset($this->documentIdentifiers[$oid])) {
1422 503
            $this->addToIdentityMap($document);
1423 503
        }
1424 506
    }
1425
1426
    /**
1427
     * Schedules a document for upsert into the database and adds it to the
1428
     * identity map
1429
     *
1430
     * @param ClassMetadata $class
1431
     * @param object $document The document to schedule for upsert.
1432
     * @throws \InvalidArgumentException
1433
     */
1434 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1435
    {
1436 84
        $oid = spl_object_hash($document);
1437
1438 84
        if ($class->isEmbeddedDocument) {
1439
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1440
        }
1441 84
        if (isset($this->documentUpdates[$oid])) {
1442
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1443
        }
1444 84
        if (isset($this->documentDeletions[$oid])) {
1445
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1446
        }
1447 84
        if (isset($this->documentUpserts[$oid])) {
1448
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1449
        }
1450
1451 84
        $this->documentUpserts[$oid] = $document;
1452 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1453 84
        $this->addToIdentityMap($document);
1454 84
    }
1455
1456
    /**
1457
     * Checks whether a document is scheduled for insertion.
1458
     *
1459
     * @param object $document
1460
     * @return boolean
1461
     */
1462 70
    public function isScheduledForInsert($document)
1463
    {
1464 70
        return isset($this->documentInsertions[spl_object_hash($document)]);
1465
    }
1466
1467
    /**
1468
     * Checks whether a document is scheduled for upsert.
1469
     *
1470
     * @param object $document
1471
     * @return boolean
1472
     */
1473 5
    public function isScheduledForUpsert($document)
1474
    {
1475 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1476
    }
1477
1478
    /**
1479
     * Schedules a document for being updated.
1480
     *
1481
     * @param object $document The document to schedule for being updated.
1482
     * @throws \InvalidArgumentException
1483
     */
1484 223
    public function scheduleForUpdate($document)
1485
    {
1486 223
        $oid = spl_object_hash($document);
1487 223
        if ( ! isset($this->documentIdentifiers[$oid])) {
1488
            throw new \InvalidArgumentException("Document has no identity.");
1489
        }
1490
1491 223
        if (isset($this->documentDeletions[$oid])) {
1492
            throw new \InvalidArgumentException("Document is removed.");
1493
        }
1494
1495 223
        if ( ! isset($this->documentUpdates[$oid])
1496 223
            && ! isset($this->documentInsertions[$oid])
1497 223
            && ! isset($this->documentUpserts[$oid])) {
1498 219
            $this->documentUpdates[$oid] = $document;
1499 219
        }
1500 223
    }
1501
1502
    /**
1503
     * Checks whether a document is registered as dirty in the unit of work.
1504
     * Note: Is not very useful currently as dirty documents are only registered
1505
     * at commit time.
1506
     *
1507
     * @param object $document
1508
     * @return boolean
1509
     */
1510 13
    public function isScheduledForUpdate($document)
1511
    {
1512 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1513
    }
1514
1515 1
    public function isScheduledForDirtyCheck($document)
1516
    {
1517 1
        $class = $this->dm->getClassMetadata(get_class($document));
1518 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1519
    }
1520
1521
    /**
1522
     * INTERNAL:
1523
     * Schedules a document for deletion.
1524
     *
1525
     * @param object $document
1526
     */
1527 67
    public function scheduleForDelete($document)
1528
    {
1529 67
        $oid = spl_object_hash($document);
1530
1531 67
        if (isset($this->documentInsertions[$oid])) {
1532 2
            if ($this->isInIdentityMap($document)) {
1533 2
                $this->removeFromIdentityMap($document);
1534 2
            }
1535 2
            unset($this->documentInsertions[$oid]);
1536 2
            return; // document has not been persisted yet, so nothing more to do.
1537
        }
1538
1539 66
        if ( ! $this->isInIdentityMap($document)) {
1540 1
            return; // ignore
1541
        }
1542
1543 65
        $this->removeFromIdentityMap($document);
1544 65
        $this->documentStates[$oid] = self::STATE_REMOVED;
1545
1546 65
        if (isset($this->documentUpdates[$oid])) {
1547
            unset($this->documentUpdates[$oid]);
1548
        }
1549 65
        if ( ! isset($this->documentDeletions[$oid])) {
1550 65
            $this->documentDeletions[$oid] = $document;
1551 65
        }
1552 65
    }
1553
1554
    /**
1555
     * Checks whether a document is registered as removed/deleted with the unit
1556
     * of work.
1557
     *
1558
     * @param object $document
1559
     * @return boolean
1560
     */
1561 8
    public function isScheduledForDelete($document)
1562
    {
1563 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1564
    }
1565
1566
    /**
1567
     * Checks whether a document is scheduled for insertion, update or deletion.
1568
     *
1569
     * @param $document
1570
     * @return boolean
1571
     */
1572 224
    public function isDocumentScheduled($document)
1573
    {
1574 224
        $oid = spl_object_hash($document);
1575 224
        return isset($this->documentInsertions[$oid]) ||
1576 121
            isset($this->documentUpserts[$oid]) ||
1577 112
            isset($this->documentUpdates[$oid]) ||
1578 224
            isset($this->documentDeletions[$oid]);
1579
    }
1580
1581
    /**
1582
     * INTERNAL:
1583
     * Registers a document in the identity map.
1584
     *
1585
     * Note that documents in a hierarchy are registered with the class name of
1586
     * the root document. Identifiers are serialized before being used as array
1587
     * keys to allow differentiation of equal, but not identical, values.
1588
     *
1589
     * @ignore
1590
     * @param object $document  The document to register.
1591
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1592
     *                  the document in question is already managed.
1593
     */
1594 598
    public function addToIdentityMap($document)
1595
    {
1596 598
        $class = $this->dm->getClassMetadata(get_class($document));
1597 598
        $id = $this->getIdForIdentityMap($document);
1598
1599 598
        if (isset($this->identityMap[$class->name][$id])) {
1600 53
            return false;
1601
        }
1602
1603 598
        $this->identityMap[$class->name][$id] = $document;
1604
1605 598
        if ($document instanceof NotifyPropertyChanged &&
1606 598
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1607 3
            $document->addPropertyChangedListener($this);
1608 3
        }
1609
1610 598
        return true;
1611
    }
1612
1613
    /**
1614
     * Gets the state of a document with regard to the current unit of work.
1615
     *
1616
     * @param object   $document
1617
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1618
     *                         This parameter can be set to improve performance of document state detection
1619
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1620
     *                         is either known or does not matter for the caller of the method.
1621
     * @return int The document state.
1622
     */
1623 574
    public function getDocumentState($document, $assume = null)
1624
    {
1625 574
        $oid = spl_object_hash($document);
1626
1627 574
        if (isset($this->documentStates[$oid])) {
1628 351
            return $this->documentStates[$oid];
1629
        }
1630
1631 574
        $class = $this->dm->getClassMetadata(get_class($document));
1632
1633 574
        if ($class->isEmbeddedDocument) {
1634 175
            return self::STATE_NEW;
1635
        }
1636
1637 571
        if ($assume !== null) {
1638 568
            return $assume;
1639
        }
1640
1641
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1642
         * known. Note that you cannot remember the NEW or DETACHED state in
1643
         * _documentStates since the UoW does not hold references to such
1644
         * objects and the object hash can be reused. More generally, because
1645
         * the state may "change" between NEW/DETACHED without the UoW being
1646
         * aware of it.
1647
         */
1648 4
        $id = $class->getIdentifierObject($document);
1649
1650 4
        if ($id === null) {
1651 2
            return self::STATE_NEW;
1652
        }
1653
1654
        // Check for a version field, if available, to avoid a DB lookup.
1655 2
        if ($class->isVersioned) {
1656
            return ($class->getFieldValue($document, $class->versionField))
1657
                ? self::STATE_DETACHED
1658
                : self::STATE_NEW;
1659
        }
1660
1661
        // Last try before DB lookup: check the identity map.
1662 2
        if ($this->tryGetById($id, $class)) {
1663 1
            return self::STATE_DETACHED;
1664
        }
1665
1666
        // DB lookup
1667 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1668 1
            return self::STATE_DETACHED;
1669
        }
1670
1671 1
        return self::STATE_NEW;
1672
    }
1673
1674
    /**
1675
     * INTERNAL:
1676
     * Removes a document from the identity map. This effectively detaches the
1677
     * document from the persistence management of Doctrine.
1678
     *
1679
     * @ignore
1680
     * @param object $document
1681
     * @throws \InvalidArgumentException
1682
     * @return boolean
1683
     */
1684 76
    public function removeFromIdentityMap($document)
1685
    {
1686 76
        $oid = spl_object_hash($document);
1687
1688
        // Check if id is registered first
1689 76
        if ( ! isset($this->documentIdentifiers[$oid])) {
1690
            return false;
1691
        }
1692
1693 76
        $class = $this->dm->getClassMetadata(get_class($document));
1694 76
        $id = $this->getIdForIdentityMap($document);
1695
1696 76
        if (isset($this->identityMap[$class->name][$id])) {
1697 76
            unset($this->identityMap[$class->name][$id]);
1698 76
            $this->documentStates[$oid] = self::STATE_DETACHED;
1699 76
            return true;
1700
        }
1701
1702
        return false;
1703
    }
1704
1705
    /**
1706
     * INTERNAL:
1707
     * Gets a document in the identity map by its identifier hash.
1708
     *
1709
     * @ignore
1710
     * @param mixed         $id    Document identifier
1711
     * @param ClassMetadata $class Document class
1712
     * @return object
1713
     * @throws InvalidArgumentException if the class does not have an identifier
1714
     */
1715 31
    public function getById($id, ClassMetadata $class)
1716
    {
1717 31
        if ( ! $class->identifier) {
1718
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1719
        }
1720
1721 31
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1722
1723 31
        return $this->identityMap[$class->name][$serializedId];
1724
    }
1725
1726
    /**
1727
     * INTERNAL:
1728
     * Tries to get a document by its identifier hash. If no document is found
1729
     * for the given hash, FALSE is returned.
1730
     *
1731
     * @ignore
1732
     * @param mixed         $id    Document identifier
1733
     * @param ClassMetadata $class Document class
1734
     * @return mixed The found document or FALSE.
1735
     * @throws InvalidArgumentException if the class does not have an identifier
1736
     */
1737 290
    public function tryGetById($id, ClassMetadata $class)
1738
    {
1739 290
        if ( ! $class->identifier) {
1740
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1741
        }
1742
1743 290
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1744
1745 290
        return isset($this->identityMap[$class->name][$serializedId]) ?
1746 290
            $this->identityMap[$class->name][$serializedId] : false;
1747
    }
1748
1749
    /**
1750
     * Schedules a document for dirty-checking at commit-time.
1751
     *
1752
     * @param object $document The document to schedule for dirty-checking.
1753
     * @todo Rename: scheduleForSynchronization
1754
     */
1755 2
    public function scheduleForDirtyCheck($document)
1756
    {
1757 2
        $class = $this->dm->getClassMetadata(get_class($document));
1758 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1759 2
    }
1760
1761
    /**
1762
     * Checks whether a document is registered in the identity map.
1763
     *
1764
     * @param object $document
1765
     * @return boolean
1766
     */
1767 76
    public function isInIdentityMap($document)
1768
    {
1769 76
        $oid = spl_object_hash($document);
1770
1771 76
        if ( ! isset($this->documentIdentifiers[$oid])) {
1772 4
            return false;
1773
        }
1774
1775 75
        $class = $this->dm->getClassMetadata(get_class($document));
1776 75
        $id = $this->getIdForIdentityMap($document);
1777
1778 75
        return isset($this->identityMap[$class->name][$id]);
1779
    }
1780
1781
    /**
1782
     * @param object $document
1783
     * @return string
1784
     */
1785 598
    private function getIdForIdentityMap($document)
1786
    {
1787 598
        $class = $this->dm->getClassMetadata(get_class($document));
1788
1789 598
        if ( ! $class->identifier) {
1790 145
            $id = spl_object_hash($document);
1791 145
        } else {
1792 597
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1793 597
            $id = serialize($class->getDatabaseIdentifierValue($id));
1794
        }
1795
1796 598
        return $id;
1797
    }
1798
1799
    /**
1800
     * INTERNAL:
1801
     * Checks whether an identifier exists in the identity map.
1802
     *
1803
     * @ignore
1804
     * @param string $id
1805
     * @param string $rootClassName
1806
     * @return boolean
1807
     */
1808
    public function containsId($id, $rootClassName)
1809
    {
1810
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1811
    }
1812
1813
    /**
1814
     * Persists a document as part of the current unit of work.
1815
     *
1816
     * @param object $document The document to persist.
1817
     * @throws MongoDBException If trying to persist MappedSuperclass.
1818
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1819
     */
1820 569
    public function persist($document)
1821
    {
1822 569
        $class = $this->dm->getClassMetadata(get_class($document));
1823 569
        if ($class->isMappedSuperclass) {
1824 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1825
        }
1826 568
        $visited = array();
1827 568
        $this->doPersist($document, $visited);
1828 564
    }
1829
1830
    /**
1831
     * Saves a document as part of the current unit of work.
1832
     * This method is internally called during save() cascades as it tracks
1833
     * the already visited documents to prevent infinite recursions.
1834
     *
1835
     * NOTE: This method always considers documents that are not yet known to
1836
     * this UnitOfWork as NEW.
1837
     *
1838
     * @param object $document The document to persist.
1839
     * @param array $visited The already visited documents.
1840
     * @throws \InvalidArgumentException
1841
     * @throws MongoDBException
1842
     */
1843 568
    private function doPersist($document, array &$visited)
1844
    {
1845 568
        $oid = spl_object_hash($document);
1846 568
        if (isset($visited[$oid])) {
1847 24
            return; // Prevent infinite recursion
1848
        }
1849
1850 568
        $visited[$oid] = $document; // Mark visited
1851
1852 568
        $class = $this->dm->getClassMetadata(get_class($document));
1853
1854 568
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1855
        switch ($documentState) {
1856 568
            case self::STATE_MANAGED:
1857
                // Nothing to do, except if policy is "deferred explicit"
1858 44
                if ($class->isChangeTrackingDeferredExplicit()) {
1859
                    $this->scheduleForDirtyCheck($document);
1860
                }
1861 44
                break;
1862 568
            case self::STATE_NEW:
1863 568
                $this->persistNew($class, $document);
1864 566
                break;
1865
1866 2
            case self::STATE_REMOVED:
1867
                // Document becomes managed again
1868 2
                unset($this->documentDeletions[$oid]);
1869
1870 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1871 2
                break;
1872
1873
            case self::STATE_DETACHED:
1874
                throw new \InvalidArgumentException(
1875
                    "Behavior of persist() for a detached document is not yet defined.");
1876
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1877
1878
            default:
1879
                throw MongoDBException::invalidDocumentState($documentState);
1880
        }
1881
1882 566
        $this->cascadePersist($document, $visited);
1883 564
    }
1884
1885
    /**
1886
     * Deletes a document as part of the current unit of work.
1887
     *
1888
     * @param object $document The document to remove.
1889
     */
1890 66
    public function remove($document)
1891
    {
1892 66
        $visited = array();
1893 66
        $this->doRemove($document, $visited);
1894 66
    }
1895
1896
    /**
1897
     * Deletes a document as part of the current unit of work.
1898
     *
1899
     * This method is internally called during delete() cascades as it tracks
1900
     * the already visited documents to prevent infinite recursions.
1901
     *
1902
     * @param object $document The document to delete.
1903
     * @param array $visited The map of the already visited documents.
1904
     * @throws MongoDBException
1905
     */
1906 66
    private function doRemove($document, array &$visited)
1907
    {
1908 66
        $oid = spl_object_hash($document);
1909 66
        if (isset($visited[$oid])) {
1910 1
            return; // Prevent infinite recursion
1911
        }
1912
1913 66
        $visited[$oid] = $document; // mark visited
1914
1915
        /* Cascade first, because scheduleForDelete() removes the entity from
1916
         * the identity map, which can cause problems when a lazy Proxy has to
1917
         * be initialized for the cascade operation.
1918
         */
1919 66
        $this->cascadeRemove($document, $visited);
1920
1921 66
        $class = $this->dm->getClassMetadata(get_class($document));
1922 66
        $documentState = $this->getDocumentState($document);
1923
        switch ($documentState) {
1924 66
            case self::STATE_NEW:
1925 66
            case self::STATE_REMOVED:
1926
                // nothing to do
1927 1
                break;
1928 66
            case self::STATE_MANAGED:
1929 66
                if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) {
1930 8
                    $class->invokeLifecycleCallbacks(Events::preRemove, $document);
1931 8
                }
1932 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...
1933 1
                    $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm));
1934 1
                }
1935 66
                $this->scheduleForDelete($document);
1936 66
                break;
1937
            case self::STATE_DETACHED:
1938
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1939
            default:
1940
                throw MongoDBException::invalidDocumentState($documentState);
1941
        }
1942 66
    }
1943
1944
    /**
1945
     * Merges the state of the given detached document into this UnitOfWork.
1946
     *
1947
     * @param object $document
1948
     * @return object The managed copy of the document.
1949
     */
1950 13
    public function merge($document)
1951
    {
1952 13
        $visited = array();
1953
1954 13
        return $this->doMerge($document, $visited);
1955
    }
1956
1957
    /**
1958
     * Executes a merge operation on a document.
1959
     *
1960
     * @param object      $document
1961
     * @param array       $visited
1962
     * @param object|null $prevManagedCopy
1963
     * @param array|null  $assoc
1964
     *
1965
     * @return object The managed copy of the document.
1966
     *
1967
     * @throws InvalidArgumentException If the entity instance is NEW.
1968
     * @throws LockException If the document uses optimistic locking through a
1969
     *                       version attribute and the version check against the
1970
     *                       managed copy fails.
1971
     */
1972 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1973
    {
1974 13
        $oid = spl_object_hash($document);
1975
1976 13
        if (isset($visited[$oid])) {
1977 1
            return $visited[$oid]; // Prevent infinite recursion
1978
        }
1979
1980 13
        $visited[$oid] = $document; // mark visited
1981
1982 13
        $class = $this->dm->getClassMetadata(get_class($document));
1983
1984
        /* First we assume DETACHED, although it can still be NEW but we can
1985
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1986
         * an identity, we need to fetch it from the DB anyway in order to
1987
         * merge. MANAGED documents are ignored by the merge operation.
1988
         */
1989 13
        $managedCopy = $document;
1990
1991 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1992 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1993
                $document->__load();
1994
            }
1995
1996
            // Try to look the document up in the identity map.
1997 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
1998
1999 13
            if ($id === null) {
2000
                // If there is no identifier, it is actually NEW.
2001 5
                $managedCopy = $class->newInstance();
2002 5
                $this->persistNew($class, $managedCopy);
2003 5
            } else {
2004 10
                $managedCopy = $this->tryGetById($id, $class);
2005
2006 10
                if ($managedCopy) {
2007
                    // We have the document in memory already, just make sure it is not removed.
2008 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
2009
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
2010
                    }
2011 5
                } else {
2012
                    // We need to fetch the managed copy in order to merge.
2013 7
                    $managedCopy = $this->dm->find($class->name, $id);
2014
                }
2015
2016 10
                if ($managedCopy === null) {
2017
                    // If the identifier is ASSIGNED, it is NEW
2018
                    $managedCopy = $class->newInstance();
2019
                    $class->setIdentifierValue($managedCopy, $id);
2020
                    $this->persistNew($class, $managedCopy);
2021
                } else {
2022 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...
2023
                        $managedCopy->__load();
2024
                    }
2025
                }
2026
            }
2027
2028 13
            if ($class->isVersioned) {
2029
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
2030
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2031
2032
                // Throw exception if versions don't match
2033
                if ($managedCopyVersion != $documentVersion) {
2034
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
2035
                }
2036
            }
2037
2038
            // Merge state of $document into existing (managed) document
2039 13
            foreach ($class->reflClass->getProperties() as $prop) {
2040 13
                $name = $prop->name;
2041 13
                $prop->setAccessible(true);
2042 13
                if ( ! isset($class->associationMappings[$name])) {
2043 13
                    if ( ! $class->isIdentifier($name)) {
2044 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
2045 13
                    }
2046 13
                } else {
2047 13
                    $assoc2 = $class->associationMappings[$name];
2048
2049 13
                    if ($assoc2['type'] === 'one') {
2050 5
                        $other = $prop->getValue($document);
2051
2052 5
                        if ($other === null) {
2053 2
                            $prop->setValue($managedCopy, null);
2054 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...
2055
                            // Do not merge fields marked lazy that have not been fetched
2056 1
                            continue;
2057 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
2058
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
2059
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
2060
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
2061
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
2062
                                $relatedId = $targetClass->getIdentifierObject($other);
2063
2064
                                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...
2065
                                    $other = $this->dm->find($targetClass->name, $relatedId);
2066
                                } else {
2067
                                    $other = $this
2068
                                        ->dm
2069
                                        ->getProxyFactory()
2070
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
2071
                                    $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...
2072
                                }
2073
                            }
2074
2075
                            $prop->setValue($managedCopy, $other);
2076
                        }
2077 4
                    } else {
2078 10
                        $mergeCol = $prop->getValue($document);
2079
2080 10
                        if ($mergeCol instanceof PersistentCollection && ! $mergeCol->isInitialized()) {
2081
                            /* Do not merge fields marked lazy that have not
2082
                             * been fetched. Keep the lazy persistent collection
2083
                             * of the managed copy.
2084
                             */
2085 3
                            continue;
2086
                        }
2087
2088 7
                        $managedCol = $prop->getValue($managedCopy);
2089
2090 7
                        if ( ! $managedCol) {
2091 2
                            $managedCol = new PersistentCollection(new ArrayCollection(), $this->dm, $this);
2092 2
                            $managedCol->setOwner($managedCopy, $assoc2);
2093 2
                            $prop->setValue($managedCopy, $managedCol);
2094 2
                            $this->originalDocumentData[$oid][$name] = $managedCol;
2095 2
                        }
2096
2097
                        /* Note: do not process association's target documents.
2098
                         * They will be handled during the cascade. Initialize
2099
                         * and, if necessary, clear $managedCol for now.
2100
                         */
2101 7
                        if ($assoc2['isCascadeMerge']) {
2102 7
                            $managedCol->initialize();
2103
2104
                            // If $managedCol differs from the merged collection, clear and set dirty
2105 7
                            if ( ! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
2106 2
                                $managedCol->unwrap()->clear();
2107 2
                                $managedCol->setDirty(true);
2108
2109 2
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
2110
                                    $this->scheduleForDirtyCheck($managedCopy);
2111
                                }
2112 2
                            }
2113 7
                        }
2114
                    }
2115
                }
2116
2117 13
                if ($class->isChangeTrackingNotify()) {
2118
                    // Just treat all properties as changed, there is no other choice.
2119
                    $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
2120
                }
2121 13
            }
2122
2123 13
            if ($class->isChangeTrackingDeferredExplicit()) {
2124
                $this->scheduleForDirtyCheck($document);
2125
            }
2126 13
        }
2127
2128 13
        if ($prevManagedCopy !== null) {
2129 6
            $assocField = $assoc['fieldName'];
2130 6
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
2131
2132 6
            if ($assoc['type'] === 'one') {
2133 2
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
2134 2
            } else {
2135 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
2136
2137 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
2138 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
2139 1
                }
2140
            }
2141 6
        }
2142
2143
        // Mark the managed copy visited as well
2144 13
        $visited[spl_object_hash($managedCopy)] = true;
2145
2146 13
        $this->cascadeMerge($document, $managedCopy, $visited);
2147
2148 13
        return $managedCopy;
2149
    }
2150
2151
    /**
2152
     * Detaches a document from the persistence management. It's persistence will
2153
     * no longer be managed by Doctrine.
2154
     *
2155
     * @param object $document The document to detach.
2156
     */
2157 9
    public function detach($document)
2158
    {
2159 9
        $visited = array();
2160 9
        $this->doDetach($document, $visited);
2161 9
    }
2162
2163
    /**
2164
     * Executes a detach operation on the given document.
2165
     *
2166
     * @param object $document
2167
     * @param array $visited
2168
     * @internal This method always considers documents with an assigned identifier as DETACHED.
2169
     */
2170 12
    private function doDetach($document, array &$visited)
2171
    {
2172 12
        $oid = spl_object_hash($document);
2173 12
        if (isset($visited[$oid])) {
2174 4
            return; // Prevent infinite recursion
2175
        }
2176
2177 12
        $visited[$oid] = $document; // mark visited
2178
2179 12
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
2180 12
            case self::STATE_MANAGED:
2181 12
                $this->removeFromIdentityMap($document);
2182 12
                unset($this->documentInsertions[$oid], $this->documentUpdates[$oid],
2183 12
                    $this->documentDeletions[$oid], $this->documentIdentifiers[$oid],
2184 12
                    $this->documentStates[$oid], $this->originalDocumentData[$oid],
2185 12
                    $this->parentAssociations[$oid], $this->documentUpserts[$oid],
2186 12
                    $this->hasScheduledCollections[$oid]);
2187 12
                break;
2188 4
            case self::STATE_NEW:
2189 4
            case self::STATE_DETACHED:
2190 4
                return;
2191 12
        }
2192
2193 12
        $this->cascadeDetach($document, $visited);
2194 12
    }
2195
2196
    /**
2197
     * Refreshes the state of the given document from the database, overwriting
2198
     * any local, unpersisted changes.
2199
     *
2200
     * @param object $document The document to refresh.
2201
     * @throws \InvalidArgumentException If the document is not MANAGED.
2202
     */
2203 20
    public function refresh($document)
2204
    {
2205 20
        $visited = array();
2206 20
        $this->doRefresh($document, $visited);
2207 19
    }
2208
2209
    /**
2210
     * Executes a refresh operation on a document.
2211
     *
2212
     * @param object $document The document to refresh.
2213
     * @param array $visited The already visited documents during cascades.
2214
     * @throws \InvalidArgumentException If the document is not MANAGED.
2215
     */
2216 20
    private function doRefresh($document, array &$visited)
2217
    {
2218 20
        $oid = spl_object_hash($document);
2219 20
        if (isset($visited[$oid])) {
2220
            return; // Prevent infinite recursion
2221
        }
2222
2223 20
        $visited[$oid] = $document; // mark visited
2224
2225 20
        $class = $this->dm->getClassMetadata(get_class($document));
2226
2227 20
        if ( ! $class->isEmbeddedDocument) {
2228 20
            if ($this->getDocumentState($document) == self::STATE_MANAGED) {
2229 19
                $id = $class->getDatabaseIdentifierValue($this->documentIdentifiers[$oid]);
2230 19
                $this->getDocumentPersister($class->name)->refresh($id, $document);
2231 19
            } else {
2232 1
                throw new \InvalidArgumentException("Document is not MANAGED.");
2233
            }
2234 19
        }
2235
2236 19
        $this->cascadeRefresh($document, $visited);
2237 19
    }
2238
2239
    /**
2240
     * Cascades a refresh operation to associated documents.
2241
     *
2242
     * @param object $document
2243
     * @param array $visited
2244
     */
2245 19
    private function cascadeRefresh($document, array &$visited)
2246
    {
2247 19
        $class = $this->dm->getClassMetadata(get_class($document));
2248
2249 19
        $associationMappings = array_filter(
2250 19
            $class->associationMappings,
2251
            function ($assoc) { return $assoc['isCascadeRefresh']; }
2252 19
        );
2253
2254 19
        foreach ($associationMappings as $mapping) {
2255 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2256 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2257 15
                if ($relatedDocuments instanceof PersistentCollection) {
2258
                    // Unwrap so that foreach() does not initialize
2259 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2260 15
                }
2261 15
                foreach ($relatedDocuments as $relatedDocument) {
2262
                    $this->doRefresh($relatedDocument, $visited);
2263 15
                }
2264 15
            } elseif ($relatedDocuments !== null) {
2265 2
                $this->doRefresh($relatedDocuments, $visited);
2266 2
            }
2267 19
        }
2268 19
    }
2269
2270
    /**
2271
     * Cascades a detach operation to associated documents.
2272
     *
2273
     * @param object $document
2274
     * @param array $visited
2275
     */
2276 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...
2277
    {
2278 12
        $class = $this->dm->getClassMetadata(get_class($document));
2279 12
        foreach ($class->fieldMappings as $mapping) {
2280 12
            if ( ! $mapping['isCascadeDetach']) {
2281 12
                continue;
2282
            }
2283 7
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2284 7
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2285 7
                if ($relatedDocuments instanceof PersistentCollection) {
2286
                    // Unwrap so that foreach() does not initialize
2287 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2288 6
                }
2289 7
                foreach ($relatedDocuments as $relatedDocument) {
2290 5
                    $this->doDetach($relatedDocument, $visited);
2291 7
                }
2292 7
            } elseif ($relatedDocuments !== null) {
2293 5
                $this->doDetach($relatedDocuments, $visited);
2294 5
            }
2295 12
        }
2296 12
    }
2297
    /**
2298
     * Cascades a merge operation to associated documents.
2299
     *
2300
     * @param object $document
2301
     * @param object $managedCopy
2302
     * @param array $visited
2303
     */
2304 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2305
    {
2306 13
        $class = $this->dm->getClassMetadata(get_class($document));
2307
2308 13
        $associationMappings = array_filter(
2309 13
            $class->associationMappings,
2310
            function ($assoc) { return $assoc['isCascadeMerge']; }
2311 13
        );
2312
2313 13
        foreach ($associationMappings as $assoc) {
2314 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2315
2316 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2317 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2318
                    // Collections are the same, so there is nothing to do
2319
                    continue;
2320
                }
2321
2322 8
                if ($relatedDocuments instanceof PersistentCollection) {
2323
                    // Unwrap so that foreach() does not initialize
2324 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2325 6
                }
2326
2327 8
                foreach ($relatedDocuments as $relatedDocument) {
2328 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2329 8
                }
2330 12
            } elseif ($relatedDocuments !== null) {
2331 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2332 3
            }
2333 13
        }
2334 13
    }
2335
2336
    /**
2337
     * Cascades the save operation to associated documents.
2338
     *
2339
     * @param object $document
2340
     * @param array $visited
2341
     */
2342 566
    private function cascadePersist($document, array &$visited)
2343
    {
2344 566
        $class = $this->dm->getClassMetadata(get_class($document));
2345
2346 566
        $associationMappings = array_filter(
2347 566
            $class->associationMappings,
2348
            function ($assoc) { return $assoc['isCascadePersist']; }
2349 566
        );
2350
2351 566
        foreach ($associationMappings as $fieldName => $mapping) {
2352 389
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2353
2354 389
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2355 339
                if ($relatedDocuments instanceof PersistentCollection) {
2356 17
                    if ($relatedDocuments->getOwner() !== $document) {
2357 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2358 2
                    }
2359
                    // Unwrap so that foreach() does not initialize
2360 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2361 17
                }
2362
2363 339
                $count = 0;
2364 339
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2365 187
                    if ( ! empty($mapping['embedded'])) {
2366 113
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2367 113
                        if ($knownParent && $knownParent !== $document) {
2368 4
                            $relatedDocument = clone $relatedDocument;
2369 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2370 4
                        }
2371 113
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2372 113
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2373 113
                    }
2374 187
                    $this->doPersist($relatedDocument, $visited);
2375 338
                }
2376 389
            } elseif ($relatedDocuments !== null) {
2377 120
                if ( ! empty($mapping['embedded'])) {
2378 66
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2379 66
                    if ($knownParent && $knownParent !== $document) {
2380 5
                        $relatedDocuments = clone $relatedDocuments;
2381 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2382 5
                    }
2383 66
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2384 66
                }
2385 120
                $this->doPersist($relatedDocuments, $visited);
2386 119
            }
2387 565
        }
2388 564
    }
2389
2390
    /**
2391
     * Cascades the delete operation to associated documents.
2392
     *
2393
     * @param object $document
2394
     * @param array $visited
2395
     */
2396 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...
2397
    {
2398 66
        $class = $this->dm->getClassMetadata(get_class($document));
2399 66
        foreach ($class->fieldMappings as $mapping) {
2400 66
            if ( ! $mapping['isCascadeRemove']) {
2401 66
                continue;
2402
            }
2403 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...
2404 2
                $document->__load();
2405 2
            }
2406
2407 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2408 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2409
                // If its a PersistentCollection initialization is intended! No unwrap!
2410 24
                foreach ($relatedDocuments as $relatedDocument) {
2411 13
                    $this->doRemove($relatedDocument, $visited);
2412 24
                }
2413 33
            } elseif ($relatedDocuments !== null) {
2414 12
                $this->doRemove($relatedDocuments, $visited);
2415 12
            }
2416 66
        }
2417 66
    }
2418
2419
    /**
2420
     * Acquire a lock on the given document.
2421
     *
2422
     * @param object $document
2423
     * @param int $lockMode
2424
     * @param int $lockVersion
2425
     * @throws LockException
2426
     * @throws \InvalidArgumentException
2427
     */
2428 9
    public function lock($document, $lockMode, $lockVersion = null)
2429
    {
2430 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2431 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2432
        }
2433
2434 8
        $documentName = get_class($document);
2435 8
        $class = $this->dm->getClassMetadata($documentName);
2436
2437 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2438 3
            if ( ! $class->isVersioned) {
2439 1
                throw LockException::notVersioned($documentName);
2440
            }
2441
2442 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...
2443 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2444 2
                if ($documentVersion != $lockVersion) {
2445 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2446
                }
2447 1
            }
2448 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2449 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2450 5
        }
2451 6
    }
2452
2453
    /**
2454
     * Releases a lock on the given document.
2455
     *
2456
     * @param object $document
2457
     * @throws \InvalidArgumentException
2458
     */
2459 1
    public function unlock($document)
2460
    {
2461 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2462
            throw new \InvalidArgumentException("Document is not MANAGED.");
2463
        }
2464 1
        $documentName = get_class($document);
2465 1
        $this->getDocumentPersister($documentName)->unlock($document);
2466 1
    }
2467
2468
    /**
2469
     * Clears the UnitOfWork.
2470
     *
2471
     * @param string|null $documentName if given, only documents of this type will get detached.
2472
     */
2473 386
    public function clear($documentName = null)
2474
    {
2475 386
        if ($documentName === null) {
2476 380
            $this->identityMap =
2477 380
            $this->documentIdentifiers =
2478 380
            $this->originalDocumentData =
2479 380
            $this->documentChangeSets =
2480 380
            $this->documentStates =
2481 380
            $this->scheduledForDirtyCheck =
2482 380
            $this->documentInsertions =
2483 380
            $this->documentUpserts =
2484 380
            $this->documentUpdates =
2485 380
            $this->documentDeletions =
2486 380
            $this->collectionUpdates =
2487 380
            $this->collectionDeletions =
2488 380
            $this->parentAssociations =
2489 380
            $this->orphanRemovals = 
2490 380
            $this->hasScheduledCollections = array();
2491 380
        } else {
2492 6
            $visited = array();
2493 6
            foreach ($this->identityMap as $className => $documents) {
2494 6
                if ($className === $documentName) {
2495 3
                    foreach ($documents as $document) {
2496 3
                        $this->doDetach($document, $visited, true);
0 ignored issues
show
Unused Code introduced by
The call to UnitOfWork::doDetach() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2497 3
                    }
2498 3
                }
2499 6
            }
2500
        }
2501
2502 386 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...
2503
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2504
        }
2505 386
    }
2506
2507
    /**
2508
     * INTERNAL:
2509
     * Schedules an embedded document for removal. The remove() operation will be
2510
     * invoked on that document at the beginning of the next commit of this
2511
     * UnitOfWork.
2512
     *
2513
     * @ignore
2514
     * @param object $document
2515
     */
2516 47
    public function scheduleOrphanRemoval($document)
2517
    {
2518 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2519 47
    }
2520
2521
    /**
2522
     * INTERNAL:
2523
     * Unschedules an embedded or referenced object for removal.
2524
     *
2525
     * @ignore
2526
     * @param object $document
2527
     */
2528 103
    public function unscheduleOrphanRemoval($document)
2529
    {
2530 103
        $oid = spl_object_hash($document);
2531 103
        if (isset($this->orphanRemovals[$oid])) {
2532 1
            unset($this->orphanRemovals[$oid]);
2533 1
        }
2534 103
    }
2535
2536
    /**
2537
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2538
     *  1) sets owner if it was cloned
2539
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2540
     *  3) NOP if state is OK
2541
     * Returned collection should be used from now on (only important with 2nd point)
2542
     *
2543
     * @param PersistentCollection $coll
2544
     * @param object $document
2545
     * @param ClassMetadata $class
2546
     * @param string $propName
2547
     * @return PersistentCollection
2548
     */
2549 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2550
    {
2551 8
        $owner = $coll->getOwner();
2552 8
        if ($owner === null) { // cloned
2553 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2554 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2555 2
            if ( ! $coll->isInitialized()) {
2556 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2557 1
            }
2558 2
            $newValue = clone $coll;
2559 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2560 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2561 2
            if ($this->isScheduledForUpdate($document)) {
2562
                // @todo following line should be superfluous once collections are stored in change sets
2563
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2564
            }
2565 2
            return $newValue;
2566
        }
2567 6
        return $coll;
2568
    }
2569
2570
    /**
2571
     * INTERNAL:
2572
     * Schedules a complete collection for removal when this UnitOfWork commits.
2573
     *
2574
     * @param PersistentCollection $coll
2575
     */
2576 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2577
    {
2578 41
        $oid = spl_object_hash($coll);
2579 41
        unset($this->collectionUpdates[$oid]);
2580 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2581 41
            $this->collectionDeletions[$oid] = $coll;
2582 41
            $this->scheduleCollectionOwner($coll);
2583 41
        }
2584 41
    }
2585
2586
    /**
2587
     * Checks whether a PersistentCollection is scheduled for deletion.
2588
     *
2589
     * @param PersistentCollection $coll
2590
     * @return boolean
2591
     */
2592 106
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2593
    {
2594 106
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2595
    }
2596
    
2597
    /**
2598
     * INTERNAL:
2599
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2600
     * 
2601
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2602
     */
2603 205 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...
2604
    {
2605 205
        $oid = spl_object_hash($coll);
2606 205
        if (isset($this->collectionDeletions[$oid])) {
2607 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2608 11
            unset($this->collectionDeletions[$oid]);
2609 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2610 11
        }
2611 205
    }
2612
2613
    /**
2614
     * INTERNAL:
2615
     * Schedules a collection for update when this UnitOfWork commits.
2616
     *
2617
     * @param PersistentCollection $coll
2618
     */
2619 221
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2620
    {
2621 221
        $mapping = $coll->getMapping();
2622 221
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2623
            /* There is no need to $unset collection if it will be $set later
2624
             * This is NOP if collection is not scheduled for deletion
2625
             */
2626 40
            $this->unscheduleCollectionDeletion($coll);
2627 40
        }
2628 221
        $oid = spl_object_hash($coll);
2629 221
        if ( ! isset($this->collectionUpdates[$oid])) {
2630 221
            $this->collectionUpdates[$oid] = $coll;
2631 221
            $this->scheduleCollectionOwner($coll);
2632 221
        }
2633 221
    }
2634
    
2635
    /**
2636
     * INTERNAL:
2637
     * Unschedules a collection from being updated when this UnitOfWork commits.
2638
     * 
2639
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2640
     */
2641 205 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...
2642
    {
2643 205
        $oid = spl_object_hash($coll);
2644 205
        if (isset($this->collectionUpdates[$oid])) {
2645 195
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2646 195
            unset($this->collectionUpdates[$oid]);
2647 195
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2648 195
        }
2649 205
    }
2650
    
2651
    /**
2652
     * Checks whether a PersistentCollection is scheduled for update.
2653
     *
2654
     * @param PersistentCollection $coll
2655
     * @return boolean
2656
     */
2657 122
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2658
    {
2659 122
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2660
    }
2661
2662
    /**
2663
     * INTERNAL:
2664
     * Gets PersistentCollections that have been visited during computing change
2665
     * set of $document
2666
     *
2667
     * @param object $document
2668
     * @return PersistentCollection[]
2669
     */
2670 534
    public function getVisitedCollections($document)
2671
    {
2672 534
        $oid = spl_object_hash($document);
2673 534
        return isset($this->visitedCollections[$oid])
2674 534
                ? $this->visitedCollections[$oid]
2675 534
                : array();
2676
    }
2677
    
2678
    /**
2679
     * INTERNAL:
2680
     * Gets PersistentCollections that are scheduled to update and related to $document
2681
     * 
2682
     * @param object $document
2683
     * @return array
2684
     */
2685 534
    public function getScheduledCollections($document)
2686
    {
2687 534
        $oid = spl_object_hash($document);
2688 534
        return isset($this->hasScheduledCollections[$oid]) 
2689 534
                ? $this->hasScheduledCollections[$oid]
2690 534
                : array();
2691
    }
2692
    
2693
    /**
2694
     * Checks whether the document is related to a PersistentCollection
2695
     * scheduled for update or deletion.
2696
     *
2697
     * @param object $document
2698
     * @return boolean
2699
     */
2700 59
    public function hasScheduledCollections($document)
2701
    {
2702 59
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2703
    }
2704
    
2705
    /**
2706
     * Marks the PersistentCollection's top-level owner as having a relation to
2707
     * a collection scheduled for update or deletion.
2708
     *
2709
     * If the owner is not scheduled for any lifecycle action, it will be
2710
     * scheduled for update to ensure that versioning takes place if necessary.
2711
     *
2712
     * If the collection is nested within atomic collection, it is immediately
2713
     * unscheduled and atomic one is scheduled for update instead. This makes
2714
     * calculating update data way easier.
2715
     * 
2716
     * @param PersistentCollection $coll
2717
     */
2718 223
    private function scheduleCollectionOwner(PersistentCollection $coll)
2719
    {
2720 223
        $document = $this->getOwningDocument($coll->getOwner());
2721 223
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2722
2723 223
        if ($document !== $coll->getOwner()) {
2724 24
            $parent = $coll->getOwner();
2725 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2726 24
                list($mapping, $parent, ) = $parentAssoc;
2727 24
            }
2728 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2729 7
                $class = $this->dm->getClassMetadata(get_class($document));
2730 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2731 7
                $this->scheduleCollectionUpdate($atomicCollection);
2732 7
                $this->unscheduleCollectionDeletion($coll);
2733 7
                $this->unscheduleCollectionUpdate($coll);
2734 7
            }
2735 24
        }
2736
2737 223
        if ( ! $this->isDocumentScheduled($document)) {
2738 92
            $this->scheduleForUpdate($document);
2739 92
        }
2740 223
    }
2741
2742
    /**
2743
     * Get the top-most owning document of a given document
2744
     *
2745
     * If a top-level document is provided, that same document will be returned.
2746
     * For an embedded document, we will walk through parent associations until
2747
     * we find a top-level document.
2748
     *
2749
     * @param object $document
2750
     * @throws \UnexpectedValueException when a top-level document could not be found
2751
     * @return object
2752
     */
2753 225
    public function getOwningDocument($document)
2754
    {
2755 225
        $class = $this->dm->getClassMetadata(get_class($document));
2756 225
        while ($class->isEmbeddedDocument) {
2757 38
            $parentAssociation = $this->getParentAssociation($document);
2758
2759 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...
2760
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2761
            }
2762
2763 38
            list(, $document, ) = $parentAssociation;
2764 38
            $class = $this->dm->getClassMetadata(get_class($document));
2765 38
        }
2766
2767 225
        return $document;
2768
    }
2769
2770
    /**
2771
     * Gets the class name for an association (embed or reference) with respect
2772
     * to any discriminator value.
2773
     *
2774
     * @param array      $mapping Field mapping for the association
2775
     * @param array|null $data    Data for the embedded document or reference
2776
     */
2777 207
    public function getClassNameForAssociation(array $mapping, $data)
2778
    {
2779 207
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2780
2781 207
        $discriminatorValue = null;
2782 207
        if (isset($discriminatorField, $data[$discriminatorField])) {
2783 21
            $discriminatorValue = $data[$discriminatorField];
2784 207
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2785
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2786
        }
2787
2788 207
        if ($discriminatorValue !== null) {
2789 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2790 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2791 21
                : $discriminatorValue;
2792
        }
2793
2794 187
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2795
2796 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...
2797 15
            $discriminatorValue = $data[$class->discriminatorField];
2798 187
        } elseif ($class->defaultDiscriminatorValue !== null) {
2799 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2800 1
        }
2801
2802 187
        if ($discriminatorValue !== null) {
2803 16
            return isset($class->discriminatorMap[$discriminatorValue])
2804 16
                ? $class->discriminatorMap[$discriminatorValue]
2805 16
                : $discriminatorValue;
2806
        }
2807
2808 171
        return $mapping['targetDocument'];
2809
    }
2810
2811
    /**
2812
     * INTERNAL:
2813
     * Creates a document. Used for reconstitution of documents during hydration.
2814
     *
2815
     * @ignore
2816
     * @param string $className The name of the document class.
2817
     * @param array $data The data for the document.
2818
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2819
     * @param object The document to be hydrated into in case of creation
2820
     * @return object The document instance.
2821
     * @internal Highly performance-sensitive method.
2822
     */
2823 380
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2824
    {
2825 380
        $class = $this->dm->getClassMetadata($className);
2826
2827
        // @TODO figure out how to remove this
2828 380
        $discriminatorValue = null;
2829 380 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...
2830 17
            $discriminatorValue = $data[$class->discriminatorField];
2831 380
        } elseif (isset($class->defaultDiscriminatorValue)) {
2832 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2833 2
        }
2834
2835 380
        if ($discriminatorValue !== null) {
2836 18
            $className = isset($class->discriminatorMap[$discriminatorValue])
2837 18
                ? $class->discriminatorMap[$discriminatorValue]
2838 18
                : $discriminatorValue;
2839
2840 18
            $class = $this->dm->getClassMetadata($className);
2841
2842 18
            unset($data[$class->discriminatorField]);
2843 18
        }
2844
2845 380
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2846 380
        $serializedId = serialize($id);
2847
2848 380
        if (isset($this->identityMap[$class->name][$serializedId])) {
2849 89
            $document = $this->identityMap[$class->name][$serializedId];
2850 89
            $oid = spl_object_hash($document);
2851 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...
2852 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...
2853 10
                $overrideLocalValues = true;
2854 10
                if ($document instanceof NotifyPropertyChanged) {
2855
                    $document->addPropertyChangedListener($this);
2856
                }
2857 10
            } else {
2858 85
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2859
            }
2860 89
            if ($overrideLocalValues) {
2861 46
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2862 46
                $this->originalDocumentData[$oid] = $data;
2863 46
            }
2864 89
        } else {
2865 352
            if ($document === null) {
2866 352
                $document = $class->newInstance();
2867 352
            }
2868 352
            $this->registerManaged($document, $id, $data);
2869 352
            $oid = spl_object_hash($document);
2870 352
            $this->documentStates[$oid] = self::STATE_MANAGED;
2871 352
            $this->identityMap[$class->name][$serializedId] = $document;
2872 352
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2873 352
            $this->originalDocumentData[$oid] = $data;
2874
        }
2875 380
        return $document;
2876
    }
2877
2878
    /**
2879
     * Initializes (loads) an uninitialized persistent collection of a document.
2880
     *
2881
     * @param PersistentCollection $collection The collection to initialize.
2882
     */
2883 157
    public function loadCollection(PersistentCollection $collection)
2884
    {
2885 157
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2886 157
    }
2887
2888
    /**
2889
     * Gets the identity map of the UnitOfWork.
2890
     *
2891
     * @return array
2892
     */
2893
    public function getIdentityMap()
2894
    {
2895
        return $this->identityMap;
2896
    }
2897
2898
    /**
2899
     * Gets the original data of a document. The original data is the data that was
2900
     * present at the time the document was reconstituted from the database.
2901
     *
2902
     * @param object $document
2903
     * @return array
2904
     */
2905 1
    public function getOriginalDocumentData($document)
2906
    {
2907 1
        $oid = spl_object_hash($document);
2908 1
        if (isset($this->originalDocumentData[$oid])) {
2909 1
            return $this->originalDocumentData[$oid];
2910
        }
2911
        return array();
2912
    }
2913
2914
    /**
2915
     * @ignore
2916
     */
2917 50
    public function setOriginalDocumentData($document, array $data)
2918
    {
2919 50
        $this->originalDocumentData[spl_object_hash($document)] = $data;
2920 50
    }
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 353
    public function getDocumentIdentifier($document)
2943
    {
2944 353
        return isset($this->documentIdentifiers[spl_object_hash($document)]) ?
2945 353
            $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 374
    public function registerManaged($document, $id, array $data)
2989
    {
2990 374
        $oid = spl_object_hash($document);
2991 374
        $class = $this->dm->getClassMetadata(get_class($document));
2992
2993 374
        if ( ! $class->identifier || $id === null) {
2994 102
            $this->documentIdentifiers[$oid] = $oid;
2995 102
        } else {
2996 368
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2997
        }
2998
2999 374
        $this->documentStates[$oid] = self::STATE_MANAGED;
3000 374
        $this->originalDocumentData[$oid] = $data;
3001 374
        $this->addToIdentityMap($document);
3002 374
    }
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