Completed
Push — 1.0.x ( bcb4f8...f4d4ca )
by Andreas
9s
created

UnitOfWork::getOrCreateDocument()   C

Complexity

Conditions 11
Paths 72

Size

Total Lines 54
Code Lines 38

Duplication

Lines 5
Ratio 9.26 %

Code Coverage

Tests 41
CRAP Score 11.0121

Importance

Changes 0
Metric Value
dl 5
loc 54
ccs 41
cts 43
cp 0.9535
rs 6.6153
c 0
b 0
f 0
cc 11
eloc 38
nc 72
nop 4
crap 11.0121

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

Loading history...
386
            $options = array_merge($defaultOptions, $options);
387
        } else {
388 564
            $options = $defaultOptions;
389
        }
390
        // Compute changes done since last commit.
391 564
        if ($document === null) {
392 558
            $this->computeChangeSets();
393 563
        } elseif (is_object($document)) {
394 12
            $this->computeSingleDocumentChangeSet($document);
395 12
        } elseif (is_array($document)) {
396 1
            foreach ($document as $object) {
397 1
                $this->computeSingleDocumentChangeSet($object);
398 1
            }
399 1
        }
400
401 562
        if ( ! ($this->documentInsertions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
408 562
        ) {
409 24
            return; // Nothing to do.
410
        }
411
412 559
        if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

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

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

Loading history...
505
    {
506 566
        foreach ($this->documentInsertions as $document) {
507 500
            $class = $this->dm->getClassMetadata(get_class($document));
508 500
            if ( ! $class->isEmbeddedDocument) {
509 497
                $this->computeChangeSet($class, $document);
510 496
            }
511 565
        }
512 565
    }
513
514
    /**
515
     * Compute changesets of all documents scheduled for upsert.
516
     *
517
     * Embedded documents will not be processed.
518
     */
519 565 View Code Duplication
    private function computeScheduleUpsertsChangeSets()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

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

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
560
            return;
561
        }
562
563
        // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
564 12
        $oid = spl_object_hash($document);
565
566 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...
567 12
            && ! isset($this->documentUpserts[$oid])
568 12
            && ! isset($this->documentDeletions[$oid])
569 12
            && isset($this->documentStates[$oid])
570 12
        ) {
571 8
            $this->computeChangeSet($class, $document);
572 8
        }
573 12
    }
574
575
    /**
576
     * Gets the changeset for a document.
577
     *
578
     * @param object $document
579
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
580
     */
581 549
    public function getDocumentChangeSet($document)
582
    {
583 549
        $oid = spl_object_hash($document);
584 549
        if (isset($this->documentChangeSets[$oid])) {
585 549
            return $this->documentChangeSets[$oid];
586
        }
587 75
        return array();
588
    }
589
590
    /**
591
     * Get a documents actual data, flattening all the objects to arrays.
592
     *
593
     * @param object $document
594
     * @return array
595
     */
596 563
    public function getDocumentActualData($document)
597
    {
598 563
        $class = $this->dm->getClassMetadata(get_class($document));
599 563
        $actualData = array();
600 563
        foreach ($class->reflFields as $name => $refProp) {
601 563
            $mapping = $class->fieldMappings[$name];
602
            // skip not saved fields
603 563
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
604 49
                continue;
605
            }
606 563
            $value = $refProp->getValue($document);
607 563
            if (isset($mapping['file']) && ! $value instanceof GridFSFile) {
608 5
                $value = new GridFSFile($value);
609 5
                $class->reflFields[$name]->setValue($document, $value);
610 5
                $actualData[$name] = $value;
611 563
            } elseif ((isset($mapping['association']) && $mapping['type'] === 'many')
612 563
                && $value !== null && ! ($value instanceof PersistentCollection)) {
613
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
614 372
                if ( ! $value instanceof Collection) {
615 124
                    $value = new ArrayCollection($value);
616 124
                }
617
618
                // Inject PersistentCollection
619 372
                $coll = new PersistentCollection($value, $this->dm, $this);
620 372
                $coll->setOwner($document, $mapping);
621 372
                $coll->setDirty( ! $value->isEmpty());
622 372
                $class->reflFields[$name]->setValue($document, $coll);
623 372
                $actualData[$name] = $coll;
624 372
            } else {
625 563
                $actualData[$name] = $value;
626
            }
627 563
        }
628 563
        return $actualData;
629
    }
630
631
    /**
632
     * Computes the changes that happened to a single document.
633
     *
634
     * Modifies/populates the following properties:
635
     *
636
     * {@link originalDocumentData}
637
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
638
     * then it was not fetched from the database and therefore we have no original
639
     * document data yet. All of the current document data is stored as the original document data.
640
     *
641
     * {@link documentChangeSets}
642
     * The changes detected on all properties of the document are stored there.
643
     * A change is a tuple array where the first entry is the old value and the second
644
     * entry is the new value of the property. Changesets are used by persisters
645
     * to INSERT/UPDATE the persistent document state.
646
     *
647
     * {@link documentUpdates}
648
     * If the document is already fully MANAGED (has been fetched from the database before)
649
     * and any changes to its properties are detected, then a reference to the document is stored
650
     * there to mark it for an update.
651
     *
652
     * @param ClassMetadata $class The class descriptor of the document.
653
     * @param object $document The document for which to compute the changes.
654
     */
655 563
    public function computeChangeSet(ClassMetadata $class, $document)
656
    {
657 563
        if ( ! $class->isInheritanceTypeNone()) {
658 172
            $class = $this->dm->getClassMetadata(get_class($document));
659 172
        }
660
661
        // Fire PreFlush lifecycle callbacks
662 563
        if ( ! empty($class->lifecycleCallbacks[Events::preFlush])) {
663 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document);
664 11
        }
665
666 563
        $this->computeOrRecomputeChangeSet($class, $document);
667 562
    }
668
669
    /**
670
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
671
     *
672
     * @param \Doctrine\ODM\MongoDB\Mapping\ClassMetadata $class
673
     * @param object $document
674
     * @param boolean $recompute
675
     */
676 563
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
677
    {
678 563
        $oid = spl_object_hash($document);
679 563
        $actualData = $this->getDocumentActualData($document);
680 563
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
681 563
        if ($isNewDocument) {
682
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
683
            // These result in an INSERT.
684 563
            $this->originalDocumentData[$oid] = $actualData;
685 563
            $changeSet = array();
686 563
            foreach ($actualData as $propName => $actualValue) {
687
                /* At this PersistentCollection shouldn't be here, probably it
688
                 * was cloned and its ownership must be fixed
689
                 */
690 563
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
691
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
692
                    $actualValue = $actualData[$propName];
693
                }
694
                // ignore inverse side of reference relationship
695 563 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
696 172
                    continue;
697
                }
698 563
                $changeSet[$propName] = array(null, $actualValue);
699 563
            }
700 563
            $this->documentChangeSets[$oid] = $changeSet;
701 563
        } else {
702
            // Document is "fully" MANAGED: it was already fully persisted before
703
            // and we have a copy of the original data
704 280
            $originalData = $this->originalDocumentData[$oid];
705 280
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
706 280
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
707 2
                $changeSet = $this->documentChangeSets[$oid];
708 2
            } else {
709 280
                $changeSet = array();
710
            }
711
712 280
            foreach ($actualData as $propName => $actualValue) {
713
                // skip not saved fields
714 280
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
715
                    continue;
716
                }
717
718 280
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
719
720
                // skip if value has not changed
721 280
                if ($orgValue === $actualValue) {
722
                    // but consider dirty GridFSFile instances as changed
723 279
                    if ( ! (isset($class->fieldMappings[$propName]['file']) && $actualValue->isDirty())) {
724 279
                        continue;
725
                    }
726 1
                }
727
728
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
729 179
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
730 10
                    if ($orgValue !== null) {
731 5
                        $this->scheduleOrphanRemoval($orgValue);
732 5
                    }
733
734 10
                    $changeSet[$propName] = array($orgValue, $actualValue);
735 10
                    continue;
736
                }
737
738
                // if owning side of reference-one relationship
739 172
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
740 12
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
741 1
                        $this->scheduleOrphanRemoval($orgValue);
742 1
                    }
743
744 12
                    $changeSet[$propName] = array($orgValue, $actualValue);
745 12
                    continue;
746
                }
747
748 163
                if ($isChangeTrackingNotify) {
749 2
                    continue;
750
                }
751
752
                // ignore inverse side of reference relationship
753 162 View Code Duplication
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
754 2
                    continue;
755
                }
756
757
                // Persistent collection was exchanged with the "originally"
758
                // created one. This can only mean it was cloned and replaced
759
                // on another document.
760 162
                if ($actualValue instanceof PersistentCollection && $actualValue->getOwner() !== $document) {
761 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
762 6
                }
763
764
                // if embed-many or reference-many relationship
765 162
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
766 25
                    $changeSet[$propName] = array($orgValue, $actualValue);
767
                    /* If original collection was exchanged with a non-empty value
768
                     * and $set will be issued, there is no need to $unset it first
769
                     */
770 25
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
771 7
                        continue;
772
                    }
773 19
                    if ($orgValue instanceof PersistentCollection) {
774 17
                        $this->scheduleCollectionDeletion($orgValue);
775 17
                    }
776 19
                    continue;
777
                }
778
779
                // skip equivalent date values
780 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
781 36
                    $dateType = Type::getType('date');
782 36
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
783 36
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
784
785 36
                    if ($dbOrgValue instanceof \MongoDate && $dbActualValue instanceof \MongoDate && $dbOrgValue == $dbActualValue) {
786 29
                        continue;
787
                    }
788 10
                }
789
790
                // regular field
791 132
                $changeSet[$propName] = array($orgValue, $actualValue);
792 280
            }
793 280
            if ($changeSet) {
794 165
                $this->documentChangeSets[$oid] = (isset($this->documentChangeSets[$oid]))
795 165
                    ? $changeSet + $this->documentChangeSets[$oid]
796 16
                    : $changeSet;
797
798 165
                $this->originalDocumentData[$oid] = $actualData;
799 165
                $this->scheduleForUpdate($document);
800 165
            }
801
        }
802
803
        // Look for changes in associations of the document
804 563
        $associationMappings = array_filter(
805 563
            $class->associationMappings,
806
            function ($assoc) { return empty($assoc['notSaved']); }
807 563
        );
808
809 563
        foreach ($associationMappings as $mapping) {
810 435
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
811
812 435
            if ($value === null) {
813 291
                continue;
814
            }
815
816 426
            $this->computeAssociationChanges($document, $mapping, $value);
817
818 425
            if (isset($mapping['reference'])) {
819 319
                continue;
820
            }
821
822 330
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value->unwrap();
823
824 330
            foreach ($values as $obj) {
825 172
                $oid2 = spl_object_hash($obj);
826
827 172
                if (isset($this->documentChangeSets[$oid2])) {
828 170
                    $this->documentChangeSets[$oid][$mapping['fieldName']] = array($value, $value);
829
830 170
                    if ( ! $isNewDocument) {
831 71
                        $this->scheduleForUpdate($document);
832 71
                    }
833
834 170
                    break;
835
                }
836 330
            }
837 562
        }
838 562
    }
839
840
    /**
841
     * Computes all the changes that have been done to documents and collections
842
     * since the last commit and stores these changes in the _documentChangeSet map
843
     * temporarily for access by the persisters, until the UoW commit is finished.
844
     */
845 561
    public function computeChangeSets()
846
    {
847 561
        $this->computeScheduleInsertsChangeSets();
848 560
        $this->computeScheduleUpsertsChangeSets();
849
850
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
851 560
        foreach ($this->identityMap as $className => $documents) {
852 560
            $class = $this->dm->getClassMetadata($className);
853 560
            if ($class->isEmbeddedDocument) {
854
                /* we do not want to compute changes to embedded documents up front
855
                 * in case embedded document was replaced and its changeset
856
                 * would corrupt data. Embedded documents' change set will
857
                 * be calculated by reachability from owning document.
858
                 */
859 161
                continue;
860
            }
861
862
            // If change tracking is explicit or happens through notification, then only compute
863
            // changes on document of that type that are explicitly marked for synchronization.
864 560
            switch (true) {
865 560
                case ($class->isChangeTrackingDeferredImplicit()):
866 559
                    $documentsToProcess = $documents;
867 559
                    break;
868
869 3
                case (isset($this->scheduledForDirtyCheck[$className])):
870 2
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
871 2
                    break;
872
873 3
                default:
874 3
                    $documentsToProcess = array();
875
876 3
            }
877
878 560
            foreach ($documentsToProcess as $document) {
879
                // Ignore uninitialized proxy objects
880 556
                if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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

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

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

Loading history...
886 556
                    && ! isset($this->documentUpserts[$oid])
887 556
                    && ! isset($this->documentDeletions[$oid])
888 556
                    && isset($this->documentStates[$oid])
889 556
                ) {
890 265
                    $this->computeChangeSet($class, $document);
891 265
                }
892 560
            }
893 560
        }
894 560
    }
895
896
    /**
897
     * Computes the changes of an association.
898
     *
899
     * @param object $parentDocument
900
     * @param array $assoc
901
     * @param mixed $value The value of the association.
902
     * @throws \InvalidArgumentException
903
     */
904 426
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
905
    {
906 426
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
907 426
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
908 426
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
909
910 426
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
911 8
            return;
912
        }
913
914 425
        if ($value instanceof PersistentCollection && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
915 231
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
916 227
                $this->scheduleCollectionUpdate($value);
917 227
            }
918 231
            $topmostOwner = $this->getOwningDocument($value->getOwner());
919 231
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
920 231
            if ( ! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
921 135
                $value->initialize();
922 135
                foreach ($value->getDeletedDocuments() as $orphan) {
923 21
                    $this->scheduleOrphanRemoval($orphan);
924 135
                }
925 135
            }
926 231
        }
927
928
        // Look through the documents, and in any of their associations,
929
        // for transient (new) documents, recursively. ("Persistence by reachability")
930
        // Unwrap. Uninitialized collections will simply be empty.
931 425
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? array($value) : $value->unwrap();
932
933 425
        $count = 0;
934 425
        foreach ($unwrappedValue as $key => $entry) {
935 330
            if ( ! is_object($entry)) {
936 1
                throw new \InvalidArgumentException(
937 1
                        sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
938 1
                );
939
            }
940
941 329
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
942
943 329
            $state = $this->getDocumentState($entry, self::STATE_NEW);
944
945
            // Handle "set" strategy for multi-level hierarchy
946 329
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
947 329
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
948
949 329
            $count++;
950
951
            switch ($state) {
952 329
                case self::STATE_NEW:
953 59
                    if ( ! $assoc['isCascadePersist']) {
954
                        throw new \InvalidArgumentException("A new document was found through a relationship that was not"
955
                            . " configured to cascade persist operations: " . self::objToStr($entry) . "."
956
                            . " Explicitly persist the new document or configure cascading persist operations"
957
                            . " on the relationship.");
958
                    }
959
960 59
                    $this->persistNew($targetClass, $entry);
961 59
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
962 59
                    $this->computeChangeSet($targetClass, $entry);
963 59
                    break;
964
965 325
                case self::STATE_MANAGED:
966 325
                    if ($targetClass->isEmbeddedDocument) {
967 164
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
968 164
                        if ($knownParent && $knownParent !== $parentDocument) {
969 6
                            $entry = clone $entry;
970 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
971 3
                                $class->setFieldValue($parentDocument, $assoc['name'], $entry);
972 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['name'], $entry);
973 3
                            } else {
974
                                // must use unwrapped value to not trigger orphan removal
975 6
                                $unwrappedValue[$key] = $entry;
976
                            }
977 6
                            $this->persistNew($targetClass, $entry);
978 6
                        }
979 164
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
980 164
                        $this->computeChangeSet($targetClass, $entry);
981 164
                    }
982 325
                    break;
983
984 1
                case self::STATE_REMOVED:
985
                    // Consume the $value as array (it's either an array or an ArrayAccess)
986
                    // and remove the element from Collection.
987 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
988
                        unset($value[$key]);
989
                    }
990 1
                    break;
991
992
                case self::STATE_DETACHED:
993
                    // Can actually not happen right now as we assume STATE_NEW,
994
                    // so the exception will be raised from the DBAL layer (constraint violation).
995
                    throw new \InvalidArgumentException("A detached document was found through a "
996
                        . "relationship during cascading a persist operation.");
997
998
                default:
999
                    // MANAGED associated documents are already taken into account
1000
                    // during changeset calculation anyway, since they are in the identity map.
1001
1002
            }
1003 424
        }
1004 424
    }
1005
1006
    /**
1007
     * INTERNAL:
1008
     * Computes the changeset of an individual document, independently of the
1009
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
1010
     *
1011
     * The passed document must be a managed document. If the document already has a change set
1012
     * because this method is invoked during a commit cycle then the change sets are added.
1013
     * whereby changes detected in this method prevail.
1014
     *
1015
     * @ignore
1016
     * @param ClassMetadata $class The class descriptor of the document.
1017
     * @param object $document The document for which to (re)calculate the change set.
1018
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1019
     */
1020 20
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1021
    {
1022
        // Ignore uninitialized proxy objects
1023 20
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1024 1
            return;
1025
        }
1026
1027 19
        $oid = spl_object_hash($document);
1028
1029 19
        if ( ! isset($this->documentStates[$oid]) || $this->documentStates[$oid] != self::STATE_MANAGED) {
1030
            throw new \InvalidArgumentException('Document must be managed.');
1031
        }
1032
1033 19
        if ( ! $class->isInheritanceTypeNone()) {
1034 2
            $class = $this->dm->getClassMetadata(get_class($document));
1035 2
        }
1036
1037 19
        $this->computeOrRecomputeChangeSet($class, $document, true);
1038 19
    }
1039
1040
    /**
1041
     * @param ClassMetadata $class
1042
     * @param object $document
1043
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1044
     */
1045 584
    private function persistNew(ClassMetadata $class, $document)
1046
    {
1047 584
        $oid = spl_object_hash($document);
1048 584
        if ( ! empty($class->lifecycleCallbacks[Events::prePersist])) {
1049 157
            $class->invokeLifecycleCallbacks(Events::prePersist, $document);
1050 157
        }
1051 584 View Code Duplication
        if ($this->evm->hasListeners(Events::prePersist)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1052 6
            $this->evm->dispatchEvent(Events::prePersist, new LifecycleEventArgs($document, $this->dm));
1053 6
        }
1054
1055 584
        $upsert = false;
1056 584
        if ($class->identifier) {
1057 584
            $idValue = $class->getIdentifierValue($document);
1058 584
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1059
1060 584
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1061 3
                throw new \InvalidArgumentException(sprintf(
1062 3
                    "%s uses NONE identifier generation strategy but no identifier was provided when persisting.",
1063 3
                    get_class($document)
1064 3
                ));
1065
            }
1066
1067
            // \MongoId::isValid($idValue) was introduced in 1.5.0 so it's no good
1068 583
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('/^[0-9a-f]{24}$/', $idValue)) {
1069 1
                throw new \InvalidArgumentException(sprintf(
1070 1
                    "%s uses AUTO identifier generation strategy but provided identifier is not valid MongoId.",
1071 1
                    get_class($document)
1072 1
                ));
1073
            }
1074
1075 582
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1076 511
                $idValue = $class->idGenerator->generate($this->dm, $document);
1077 511
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1078 511
                $class->setIdentifierValue($document, $idValue);
1079 511
            }
1080
1081 582
            $this->documentIdentifiers[$oid] = $idValue;
1082 582
        } else {
1083
            // this is for embedded documents without identifiers
1084 148
            $this->documentIdentifiers[$oid] = $oid;
1085
        }
1086
1087 582
        $this->documentStates[$oid] = self::STATE_MANAGED;
1088
1089 582
        if ($upsert) {
1090 81
            $this->scheduleForUpsert($class, $document);
1091 81
        } else {
1092 516
            $this->scheduleForInsert($class, $document);
1093
        }
1094 582
    }
1095
1096
    /**
1097
     * Cascades the postPersist events to embedded documents.
1098
     *
1099
     * @param ClassMetadata $class
1100
     * @param object $document
1101
     */
1102 557
    private function cascadePostPersist(ClassMetadata $class, $document)
1103
    {
1104 557
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1105
1106 557
        $embeddedMappings = array_filter(
1107 557
            $class->associationMappings,
1108
            function($assoc) { return ! empty($assoc['embedded']); }
1109 557
        );
1110
1111 557
        foreach ($embeddedMappings as $mapping) {
1112 338
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1113
1114 338
            if ($value === null) {
1115 214
                continue;
1116
            }
1117
1118 320
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1119
1120 320
            if (isset($mapping['targetDocument'])) {
1121 306
                $embeddedClass = $this->dm->getClassMetadata($mapping['targetDocument']);
1122 306
            }
1123
1124 320
            foreach ($values as $embeddedDocument) {
1125 162
                if ( ! isset($mapping['targetDocument'])) {
1126 16
                    $embeddedClass = $this->dm->getClassMetadata(get_class($embeddedDocument));
1127 16
                }
1128
1129 162
                if ( ! empty($embeddedClass->lifecycleCallbacks[Events::postPersist])) {
1130 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...
1131 9
                }
1132 162
                if ($hasPostPersistListeners) {
1133 4
                    $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($embeddedDocument, $this->dm));
1134 4
                }
1135 162
                $this->cascadePostPersist($embeddedClass, $embeddedDocument);
1136 320
            }
1137 557
         }
1138 557
     }
1139
1140
    /**
1141
     * Executes all document insertions for documents of the specified type.
1142
     *
1143
     * @param ClassMetadata $class
1144
     * @param array $documents Array of documents to insert
1145
     * @param array $options Array of options to be used with batchInsert()
1146
     */
1147 492 View Code Duplication
    private function executeInserts(ClassMetadata $class, array $documents, array $options = array())
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
1148
    {
1149 492
        $persister = $this->getDocumentPersister($class->name);
1150
1151 492
        foreach ($documents as $oid => $document) {
1152 492
            $persister->addInsert($document);
1153 492
            unset($this->documentInsertions[$oid]);
1154 492
        }
1155
1156 492
        $persister->executeInserts($options);
1157
1158 491
        $hasPostPersistLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postPersist]);
1159 491
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1160
1161 491
        foreach ($documents as $document) {
1162 491
            if ($hasPostPersistLifecycleCallbacks) {
1163 10
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1164 10
            }
1165 491
            if ($hasPostPersistListeners) {
1166 5
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1167 5
            }
1168 491
            $this->cascadePostPersist($class, $document);
1169 491
        }
1170 491
    }
1171
1172
    /**
1173
     * Executes all document upserts for documents of the specified type.
1174
     *
1175
     * @param ClassMetadata $class
1176
     * @param array $documents Array of documents to upsert
1177
     * @param array $options Array of options to be used with batchInsert()
1178
     */
1179 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...
1180
    {
1181 78
        $persister = $this->getDocumentPersister($class->name);
1182
1183
1184 78
        foreach ($documents as $oid => $document) {
1185 78
            $persister->addUpsert($document);
1186 78
            unset($this->documentUpserts[$oid]);
1187 78
        }
1188
1189 78
        $persister->executeUpserts($options);
1190
1191 78
        $hasLifecycleCallbacks = isset($class->lifecycleCallbacks[Events::postPersist]);
1192 78
        $hasListeners = $this->evm->hasListeners(Events::postPersist);
1193
1194 78
        foreach ($documents as $document) {
1195 78
            if ($hasLifecycleCallbacks) {
1196
                $class->invokeLifecycleCallbacks(Events::postPersist, $document);
1197
            }
1198 78
            if ($hasListeners) {
1199 2
                $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($document, $this->dm));
1200 2
            }
1201 78
            $this->cascadePostPersist($class, $document);
1202 78
        }
1203 78
    }
1204
1205
    /**
1206
     * Executes all document updates for documents of the specified type.
1207
     *
1208
     * @param Mapping\ClassMetadata $class
1209
     * @param array $documents Array of documents to update
1210
     * @param array $options Array of options to be used with update()
1211
     */
1212 220
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = array())
1213
    {
1214 220
        $className = $class->name;
1215 220
        $persister = $this->getDocumentPersister($className);
1216
1217 220
        $hasPreUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::preUpdate]);
1218 220
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1219 220
        $hasPostUpdateLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postUpdate]);
1220 220
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1221
1222 220
        foreach ($documents as $oid => $document) {
1223 220
            if ($hasPreUpdateLifecycleCallbacks) {
1224 12
                $class->invokeLifecycleCallbacks(Events::preUpdate, $document);
1225 12
                $this->recomputeSingleDocumentChangeSet($class, $document);
1226 12
            }
1227
1228 220
            if ($hasPreUpdateListeners) {
1229 8
                if ( ! isset($this->documentChangeSets[$oid])) {
1230
                    // only ReferenceMany collection is scheduled for update
1231 1
                    $this->documentChangeSets[$oid] = array();
1232 1
                }
1233 8
                $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1234 8
                    $document, $this->dm, $this->documentChangeSets[$oid])
1235 8
                );
1236 8
            }
1237 220
            $this->cascadePreUpdate($class, $document);
1238
1239 220
            if ( ! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1240 218
                $persister->update($document, $options);
1241 214
            }
1242
1243 216
            unset($this->documentUpdates[$oid]);
1244
1245 216
            if ($hasPostUpdateLifecycleCallbacks) {
1246 7
                $class->invokeLifecycleCallbacks(Events::postUpdate, $document);
1247 7
            }
1248 216
            if ($hasPostUpdateListeners) {
1249 8
                $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($document, $this->dm));
1250 8
            }
1251 216
            $this->cascadePostUpdate($class, $document);
1252 216
        }
1253 215
    }
1254
1255
    /**
1256
     * Cascades the preUpdate event to embedded documents.
1257
     *
1258
     * @param ClassMetadata $class
1259
     * @param object $document
1260
     */
1261 220
    private function cascadePreUpdate(ClassMetadata $class, $document)
1262
    {
1263 220
        $hasPreUpdateListeners = $this->evm->hasListeners(Events::preUpdate);
1264
1265 220
        $embeddedMappings = array_filter(
1266 220
            $class->associationMappings,
1267
            function ($assoc) { return ! empty($assoc['embedded']); }
1268 220
        );
1269
1270 220
        foreach ($embeddedMappings as $mapping) {
1271 133
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1272
1273 133
            if ($value === null) {
1274 49
                continue;
1275
            }
1276
1277 131
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1278
1279 131
            foreach ($values as $entry) {
1280 84
                $entryOid = spl_object_hash($entry);
1281 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1282
1283 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1284 47
                    continue;
1285
                }
1286
1287 67
                if (isset($this->documentInsertions[$entryOid])) {
1288 52
                    continue;
1289
                }
1290
1291 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...
1292 5
                    $entryClass->invokeLifecycleCallbacks(Events::preUpdate, $entry);
1293 5
                    $this->recomputeSingleDocumentChangeSet($entryClass, $entry);
1294 5
                }
1295 45
                if ($hasPreUpdateListeners) {
1296 3
                    $this->evm->dispatchEvent(Events::preUpdate, new Event\PreUpdateEventArgs(
1297 3
                        $entry, $this->dm, $this->documentChangeSets[$entryOid])
1298 3
                    );
1299 3
                }
1300
1301 45
                $this->cascadePreUpdate($entryClass, $entry);
1302 131
            }
1303 220
        }
1304 220
    }
1305
1306
    /**
1307
     * Cascades the postUpdate and postPersist events to embedded documents.
1308
     *
1309
     * @param ClassMetadata $class
1310
     * @param object $document
1311
     */
1312 216
    private function cascadePostUpdate(ClassMetadata $class, $document)
1313
    {
1314 216
        $hasPostPersistListeners = $this->evm->hasListeners(Events::postPersist);
1315 216
        $hasPostUpdateListeners = $this->evm->hasListeners(Events::postUpdate);
1316
1317 216
        $embeddedMappings = array_filter(
1318 216
            $class->associationMappings,
1319
            function($assoc) { return ! empty($assoc['embedded']); }
1320 216
        );
1321
1322 216
        foreach ($embeddedMappings as $mapping) {
1323 129
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
1324
1325 129
            if ($value === null) {
1326 52
                continue;
1327
            }
1328
1329 127
            $values = $mapping['type'] === ClassMetadata::ONE ? array($value) : $value;
1330
1331 127
            foreach ($values as $entry) {
1332 84
                $entryOid = spl_object_hash($entry);
1333 84
                $entryClass = $this->dm->getClassMetadata(get_class($entry));
1334
1335 84
                if ( ! isset($this->documentChangeSets[$entryOid])) {
1336 47
                    continue;
1337
                }
1338
1339 67
                if (isset($this->documentInsertions[$entryOid])) {
1340 52
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postPersist])) {
1341 1
                        $entryClass->invokeLifecycleCallbacks(Events::postPersist, $entry);
1342 1
                    }
1343 52
                    if ($hasPostPersistListeners) {
1344 3
                        $this->evm->dispatchEvent(Events::postPersist, new LifecycleEventArgs($entry, $this->dm));
1345 3
                    }
1346 52
                } else {
1347 45
                    if ( ! empty($entryClass->lifecycleCallbacks[Events::postUpdate])) {
1348 9
                        $entryClass->invokeLifecycleCallbacks(Events::postUpdate, $entry);
1349 9
                    }
1350 45
                    if ($hasPostUpdateListeners) {
1351 3
                        $this->evm->dispatchEvent(Events::postUpdate, new LifecycleEventArgs($entry, $this->dm));
1352 3
                    }
1353
                }
1354
1355 67
                $this->cascadePostUpdate($entryClass, $entry);
1356 127
            }
1357 216
        }
1358 216
    }
1359
1360
    /**
1361
     * Executes all document deletions for documents of the specified type.
1362
     *
1363
     * @param ClassMetadata $class
1364
     * @param array $documents Array of documents to delete
1365
     * @param array $options Array of options to be used with remove()
1366
     */
1367 62
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = array())
1368
    {
1369 62
        $hasPostRemoveLifecycleCallbacks = ! empty($class->lifecycleCallbacks[Events::postRemove]);
1370 62
        $hasPostRemoveListeners = $this->evm->hasListeners(Events::postRemove);
1371
1372 62
        $persister = $this->getDocumentPersister($class->name);
1373
1374 62
        foreach ($documents as $oid => $document) {
1375 62
            if ( ! $class->isEmbeddedDocument) {
1376 28
                $persister->delete($document, $options);
1377 26
            }
1378
            unset(
1379 60
                $this->documentDeletions[$oid],
1380 60
                $this->documentIdentifiers[$oid],
1381 60
                $this->originalDocumentData[$oid]
1382
            );
1383
1384
            // Clear snapshot information for any referenced PersistentCollection
1385
            // http://www.doctrine-project.org/jira/browse/MODM-95
1386 60
            foreach ($class->associationMappings as $fieldMapping) {
1387 41
                if (isset($fieldMapping['type']) && $fieldMapping['type'] === ClassMetadata::MANY) {
1388 26
                    $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1389 26
                    if ($value instanceof PersistentCollection) {
1390 22
                        $value->clearSnapshot();
1391 22
                    }
1392 26
                }
1393 60
            }
1394
1395
            // Document with this $oid after deletion treated as NEW, even if the $oid
1396
            // is obtained by a new document because the old one went out of scope.
1397 60
            $this->documentStates[$oid] = self::STATE_NEW;
1398
1399 60
            if ($hasPostRemoveLifecycleCallbacks) {
1400 8
                $class->invokeLifecycleCallbacks(Events::postRemove, $document);
1401 8
            }
1402 60
            if ($hasPostRemoveListeners) {
1403 2
                $this->evm->dispatchEvent(Events::postRemove, new LifecycleEventArgs($document, $this->dm));
1404 2
            }
1405 60
        }
1406 60
    }
1407
1408
    /**
1409
     * Schedules a document for insertion into the database.
1410
     * If the document already has an identifier, it will be added to the
1411
     * identity map.
1412
     *
1413
     * @param ClassMetadata $class
1414
     * @param object $document The document to schedule for insertion.
1415
     * @throws \InvalidArgumentException
1416
     */
1417 519
    public function scheduleForInsert(ClassMetadata $class, $document)
0 ignored issues
show
Unused Code introduced by
The parameter $class is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1418
    {
1419 519
        $oid = spl_object_hash($document);
1420
1421 519
        if (isset($this->documentUpdates[$oid])) {
1422
            throw new \InvalidArgumentException("Dirty document can not be scheduled for insertion.");
1423
        }
1424 519
        if (isset($this->documentDeletions[$oid])) {
1425
            throw new \InvalidArgumentException("Removed document can not be scheduled for insertion.");
1426
        }
1427 519
        if (isset($this->documentInsertions[$oid])) {
1428
            throw new \InvalidArgumentException("Document can not be scheduled for insertion twice.");
1429
        }
1430
1431 519
        $this->documentInsertions[$oid] = $document;
1432
1433 519
        if (isset($this->documentIdentifiers[$oid])) {
1434 516
            $this->addToIdentityMap($document);
1435 516
        }
1436 519
    }
1437
1438
    /**
1439
     * Schedules a document for upsert into the database and adds it to the
1440
     * identity map
1441
     *
1442
     * @param ClassMetadata $class
1443
     * @param object $document The document to schedule for upsert.
1444
     * @throws \InvalidArgumentException
1445
     */
1446 84
    public function scheduleForUpsert(ClassMetadata $class, $document)
1447
    {
1448 84
        $oid = spl_object_hash($document);
1449
1450 84
        if ($class->isEmbeddedDocument) {
1451
            throw new \InvalidArgumentException("Embedded document can not be scheduled for upsert.");
1452
        }
1453 84
        if (isset($this->documentUpdates[$oid])) {
1454
            throw new \InvalidArgumentException("Dirty document can not be scheduled for upsert.");
1455
        }
1456 84
        if (isset($this->documentDeletions[$oid])) {
1457
            throw new \InvalidArgumentException("Removed document can not be scheduled for upsert.");
1458
        }
1459 84
        if (isset($this->documentUpserts[$oid])) {
1460
            throw new \InvalidArgumentException("Document can not be scheduled for upsert twice.");
1461
        }
1462
1463 84
        $this->documentUpserts[$oid] = $document;
1464 84
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1465 84
        $this->addToIdentityMap($document);
1466 84
    }
1467
1468
    /**
1469
     * Checks whether a document is scheduled for insertion.
1470
     *
1471
     * @param object $document
1472
     * @return boolean
1473
     */
1474 70
    public function isScheduledForInsert($document)
1475
    {
1476 70
        return isset($this->documentInsertions[spl_object_hash($document)]);
1477
    }
1478
1479
    /**
1480
     * Checks whether a document is scheduled for upsert.
1481
     *
1482
     * @param object $document
1483
     * @return boolean
1484
     */
1485 5
    public function isScheduledForUpsert($document)
1486
    {
1487 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1488
    }
1489
1490
    /**
1491
     * Schedules a document for being updated.
1492
     *
1493
     * @param object $document The document to schedule for being updated.
1494
     * @throws \InvalidArgumentException
1495
     */
1496 229
    public function scheduleForUpdate($document)
1497
    {
1498 229
        $oid = spl_object_hash($document);
1499 229
        if ( ! isset($this->documentIdentifiers[$oid])) {
1500
            throw new \InvalidArgumentException("Document has no identity.");
1501
        }
1502
1503 229
        if (isset($this->documentDeletions[$oid])) {
1504
            throw new \InvalidArgumentException("Document is removed.");
1505
        }
1506
1507 229
        if ( ! isset($this->documentUpdates[$oid])
1508 229
            && ! isset($this->documentInsertions[$oid])
1509 229
            && ! isset($this->documentUpserts[$oid])) {
1510 225
            $this->documentUpdates[$oid] = $document;
1511 225
        }
1512 229
    }
1513
1514
    /**
1515
     * Checks whether a document is registered as dirty in the unit of work.
1516
     * Note: Is not very useful currently as dirty documents are only registered
1517
     * at commit time.
1518
     *
1519
     * @param object $document
1520
     * @return boolean
1521
     */
1522 13
    public function isScheduledForUpdate($document)
1523
    {
1524 13
        return isset($this->documentUpdates[spl_object_hash($document)]);
1525
    }
1526
1527 1
    public function isScheduledForDirtyCheck($document)
1528
    {
1529 1
        $class = $this->dm->getClassMetadata(get_class($document));
1530 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1531
    }
1532
1533
    /**
1534
     * INTERNAL:
1535
     * Schedules a document for deletion.
1536
     *
1537
     * @param object $document
1538
     */
1539 67
    public function scheduleForDelete($document)
1540
    {
1541 67
        $oid = spl_object_hash($document);
1542
1543 67
        if (isset($this->documentInsertions[$oid])) {
1544 2
            if ($this->isInIdentityMap($document)) {
1545 2
                $this->removeFromIdentityMap($document);
1546 2
            }
1547 2
            unset($this->documentInsertions[$oid]);
1548 2
            return; // document has not been persisted yet, so nothing more to do.
1549
        }
1550
1551 66
        if ( ! $this->isInIdentityMap($document)) {
1552 1
            return; // ignore
1553
        }
1554
1555 65
        $this->removeFromIdentityMap($document);
1556 65
        $this->documentStates[$oid] = self::STATE_REMOVED;
1557
1558 65
        if (isset($this->documentUpdates[$oid])) {
1559
            unset($this->documentUpdates[$oid]);
1560
        }
1561 65
        if ( ! isset($this->documentDeletions[$oid])) {
1562 65
            $this->documentDeletions[$oid] = $document;
1563 65
        }
1564 65
    }
1565
1566
    /**
1567
     * Checks whether a document is registered as removed/deleted with the unit
1568
     * of work.
1569
     *
1570
     * @param object $document
1571
     * @return boolean
1572
     */
1573 8
    public function isScheduledForDelete($document)
1574
    {
1575 8
        return isset($this->documentDeletions[spl_object_hash($document)]);
1576
    }
1577
1578
    /**
1579
     * Checks whether a document is scheduled for insertion, update or deletion.
1580
     *
1581
     * @param $document
1582
     * @return boolean
1583
     */
1584 230
    public function isDocumentScheduled($document)
1585
    {
1586 230
        $oid = spl_object_hash($document);
1587 230
        return isset($this->documentInsertions[$oid]) ||
1588 123
            isset($this->documentUpserts[$oid]) ||
1589 114
            isset($this->documentUpdates[$oid]) ||
1590 230
            isset($this->documentDeletions[$oid]);
1591
    }
1592
1593
    /**
1594
     * INTERNAL:
1595
     * Registers a document in the identity map.
1596
     *
1597
     * Note that documents in a hierarchy are registered with the class name of
1598
     * the root document. Identifiers are serialized before being used as array
1599
     * keys to allow differentiation of equal, but not identical, values.
1600
     *
1601
     * @ignore
1602
     * @param object $document  The document to register.
1603
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
1604
     *                  the document in question is already managed.
1605
     */
1606 613
    public function addToIdentityMap($document)
1607
    {
1608 613
        $class = $this->dm->getClassMetadata(get_class($document));
1609 613
        $id = $this->getIdForIdentityMap($document);
1610
1611 613
        if (isset($this->identityMap[$class->name][$id])) {
1612 53
            return false;
1613
        }
1614
1615 613
        $this->identityMap[$class->name][$id] = $document;
1616
1617 613
        if ($document instanceof NotifyPropertyChanged &&
1618 613
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1619 3
            $document->addPropertyChangedListener($this);
1620 3
        }
1621
1622 613
        return true;
1623
    }
1624
1625
    /**
1626
     * Gets the state of a document with regard to the current unit of work.
1627
     *
1628
     * @param object   $document
1629
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1630
     *                         This parameter can be set to improve performance of document state detection
1631
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1632
     *                         is either known or does not matter for the caller of the method.
1633
     * @return int The document state.
1634
     */
1635 587
    public function getDocumentState($document, $assume = null)
1636
    {
1637 587
        $oid = spl_object_hash($document);
1638
1639 587
        if (isset($this->documentStates[$oid])) {
1640 362
            return $this->documentStates[$oid];
1641
        }
1642
1643 587
        $class = $this->dm->getClassMetadata(get_class($document));
1644
1645 587
        if ($class->isEmbeddedDocument) {
1646 181
            return self::STATE_NEW;
1647
        }
1648
1649 584
        if ($assume !== null) {
1650 581
            return $assume;
1651
        }
1652
1653
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1654
         * known. Note that you cannot remember the NEW or DETACHED state in
1655
         * _documentStates since the UoW does not hold references to such
1656
         * objects and the object hash can be reused. More generally, because
1657
         * the state may "change" between NEW/DETACHED without the UoW being
1658
         * aware of it.
1659
         */
1660 4
        $id = $class->getIdentifierObject($document);
1661
1662 4
        if ($id === null) {
1663 3
            return self::STATE_NEW;
1664
        }
1665
1666
        // Check for a version field, if available, to avoid a DB lookup.
1667 2
        if ($class->isVersioned) {
1668
            return ($class->getFieldValue($document, $class->versionField))
1669
                ? self::STATE_DETACHED
1670
                : self::STATE_NEW;
1671
        }
1672
1673
        // Last try before DB lookup: check the identity map.
1674 2
        if ($this->tryGetById($id, $class)) {
1675 1
            return self::STATE_DETACHED;
1676
        }
1677
1678
        // DB lookup
1679 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1680 1
            return self::STATE_DETACHED;
1681
        }
1682
1683 1
        return self::STATE_NEW;
1684
    }
1685
1686
    /**
1687
     * INTERNAL:
1688
     * Removes a document from the identity map. This effectively detaches the
1689
     * document from the persistence management of Doctrine.
1690
     *
1691
     * @ignore
1692
     * @param object $document
1693
     * @throws \InvalidArgumentException
1694
     * @return boolean
1695
     */
1696 80
    public function removeFromIdentityMap($document)
1697
    {
1698 80
        $oid = spl_object_hash($document);
1699
1700
        // Check if id is registered first
1701 80
        if ( ! isset($this->documentIdentifiers[$oid])) {
1702
            return false;
1703
        }
1704
1705 80
        $class = $this->dm->getClassMetadata(get_class($document));
1706 80
        $id = $this->getIdForIdentityMap($document);
1707
1708 80
        if (isset($this->identityMap[$class->name][$id])) {
1709 80
            unset($this->identityMap[$class->name][$id]);
1710 80
            $this->documentStates[$oid] = self::STATE_DETACHED;
1711 80
            return true;
1712
        }
1713
1714
        return false;
1715
    }
1716
1717
    /**
1718
     * INTERNAL:
1719
     * Gets a document in the identity map by its identifier hash.
1720
     *
1721
     * @ignore
1722
     * @param mixed         $id    Document identifier
1723
     * @param ClassMetadata $class Document class
1724
     * @return object
1725
     * @throws InvalidArgumentException if the class does not have an identifier
1726
     */
1727 32
    public function getById($id, ClassMetadata $class)
1728
    {
1729 32
        if ( ! $class->identifier) {
1730
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1731
        }
1732
1733 32
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1734
1735 32
        return $this->identityMap[$class->name][$serializedId];
1736
    }
1737
1738
    /**
1739
     * INTERNAL:
1740
     * Tries to get a document by its identifier hash. If no document is found
1741
     * for the given hash, FALSE is returned.
1742
     *
1743
     * @ignore
1744
     * @param mixed         $id    Document identifier
1745
     * @param ClassMetadata $class Document class
1746
     * @return mixed The found document or FALSE.
1747
     * @throws InvalidArgumentException if the class does not have an identifier
1748
     */
1749 291
    public function tryGetById($id, ClassMetadata $class)
1750
    {
1751 291
        if ( ! $class->identifier) {
1752
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1753
        }
1754
1755 291
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1756
1757 291
        return isset($this->identityMap[$class->name][$serializedId]) ?
1758 291
            $this->identityMap[$class->name][$serializedId] : false;
1759
    }
1760
1761
    /**
1762
     * Schedules a document for dirty-checking at commit-time.
1763
     *
1764
     * @param object $document The document to schedule for dirty-checking.
1765
     * @todo Rename: scheduleForSynchronization
1766
     */
1767 2
    public function scheduleForDirtyCheck($document)
1768
    {
1769 2
        $class = $this->dm->getClassMetadata(get_class($document));
1770 2
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1771 2
    }
1772
1773
    /**
1774
     * Checks whether a document is registered in the identity map.
1775
     *
1776
     * @param object $document
1777
     * @return boolean
1778
     */
1779 76
    public function isInIdentityMap($document)
1780
    {
1781 76
        $oid = spl_object_hash($document);
1782
1783 76
        if ( ! isset($this->documentIdentifiers[$oid])) {
1784 4
            return false;
1785
        }
1786
1787 75
        $class = $this->dm->getClassMetadata(get_class($document));
1788 75
        $id = $this->getIdForIdentityMap($document);
1789
1790 75
        return isset($this->identityMap[$class->name][$id]);
1791
    }
1792
1793
    /**
1794
     * @param object $document
1795
     * @return string
1796
     */
1797 613
    private function getIdForIdentityMap($document)
1798
    {
1799 613
        $class = $this->dm->getClassMetadata(get_class($document));
1800
1801 613
        if ( ! $class->identifier) {
1802 151
            $id = spl_object_hash($document);
1803 151
        } else {
1804 612
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1805 612
            $id = serialize($class->getDatabaseIdentifierValue($id));
1806
        }
1807
1808 613
        return $id;
1809
    }
1810
1811
    /**
1812
     * INTERNAL:
1813
     * Checks whether an identifier exists in the identity map.
1814
     *
1815
     * @ignore
1816
     * @param string $id
1817
     * @param string $rootClassName
1818
     * @return boolean
1819
     */
1820
    public function containsId($id, $rootClassName)
1821
    {
1822
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1823
    }
1824
1825
    /**
1826
     * Persists a document as part of the current unit of work.
1827
     *
1828
     * @param object $document The document to persist.
1829
     * @throws MongoDBException If trying to persist MappedSuperclass.
1830
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1831
     */
1832 582
    public function persist($document)
1833
    {
1834 582
        $class = $this->dm->getClassMetadata(get_class($document));
1835 582
        if ($class->isMappedSuperclass) {
1836 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1837
        }
1838 581
        $visited = array();
1839 581
        $this->doPersist($document, $visited);
1840 577
    }
1841
1842
    /**
1843
     * Saves a document as part of the current unit of work.
1844
     * This method is internally called during save() cascades as it tracks
1845
     * the already visited documents to prevent infinite recursions.
1846
     *
1847
     * NOTE: This method always considers documents that are not yet known to
1848
     * this UnitOfWork as NEW.
1849
     *
1850
     * @param object $document The document to persist.
1851
     * @param array $visited The already visited documents.
1852
     * @throws \InvalidArgumentException
1853
     * @throws MongoDBException
1854
     */
1855 581
    private function doPersist($document, array &$visited)
1856
    {
1857 581
        $oid = spl_object_hash($document);
1858 581
        if (isset($visited[$oid])) {
1859 24
            return; // Prevent infinite recursion
1860
        }
1861
1862 581
        $visited[$oid] = $document; // Mark visited
1863
1864 581
        $class = $this->dm->getClassMetadata(get_class($document));
1865
1866 581
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1867
        switch ($documentState) {
1868 581
            case self::STATE_MANAGED:
1869
                // Nothing to do, except if policy is "deferred explicit"
1870 45
                if ($class->isChangeTrackingDeferredExplicit()) {
1871
                    $this->scheduleForDirtyCheck($document);
1872
                }
1873 45
                break;
1874 581
            case self::STATE_NEW:
1875 581
                $this->persistNew($class, $document);
1876 579
                break;
1877
1878 2
            case self::STATE_REMOVED:
1879
                // Document becomes managed again
1880 2
                unset($this->documentDeletions[$oid]);
1881
1882 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1883 2
                break;
1884
1885
            case self::STATE_DETACHED:
1886
                throw new \InvalidArgumentException(
1887
                    "Behavior of persist() for a detached document is not yet defined.");
1888
1889
            default:
1890
                throw MongoDBException::invalidDocumentState($documentState);
1891
        }
1892
1893 579
        $this->cascadePersist($document, $visited);
1894 577
    }
1895
1896
    /**
1897
     * Deletes a document as part of the current unit of work.
1898
     *
1899
     * @param object $document The document to remove.
1900
     */
1901 66
    public function remove($document)
1902
    {
1903 66
        $visited = array();
1904 66
        $this->doRemove($document, $visited);
1905 66
    }
1906
1907
    /**
1908
     * Deletes a document as part of the current unit of work.
1909
     *
1910
     * This method is internally called during delete() cascades as it tracks
1911
     * the already visited documents to prevent infinite recursions.
1912
     *
1913
     * @param object $document The document to delete.
1914
     * @param array $visited The map of the already visited documents.
1915
     * @throws MongoDBException
1916
     */
1917 66
    private function doRemove($document, array &$visited)
1918
    {
1919 66
        $oid = spl_object_hash($document);
1920 66
        if (isset($visited[$oid])) {
1921 1
            return; // Prevent infinite recursion
1922
        }
1923
1924 66
        $visited[$oid] = $document; // mark visited
1925
1926
        /* Cascade first, because scheduleForDelete() removes the entity from
1927
         * the identity map, which can cause problems when a lazy Proxy has to
1928
         * be initialized for the cascade operation.
1929
         */
1930 66
        $this->cascadeRemove($document, $visited);
1931
1932 66
        $class = $this->dm->getClassMetadata(get_class($document));
1933 66
        $documentState = $this->getDocumentState($document);
1934
        switch ($documentState) {
1935 66
            case self::STATE_NEW:
1936 66
            case self::STATE_REMOVED:
1937
                // nothing to do
1938 1
                break;
1939 66
            case self::STATE_MANAGED:
1940 66
                if ( ! empty($class->lifecycleCallbacks[Events::preRemove])) {
1941 8
                    $class->invokeLifecycleCallbacks(Events::preRemove, $document);
1942 8
                }
1943 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...
1944 1
                    $this->evm->dispatchEvent(Events::preRemove, new LifecycleEventArgs($document, $this->dm));
1945 1
                }
1946 66
                $this->scheduleForDelete($document);
1947 66
                break;
1948
            case self::STATE_DETACHED:
1949
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1950
            default:
1951
                throw MongoDBException::invalidDocumentState($documentState);
1952
        }
1953 66
    }
1954
1955
    /**
1956
     * Merges the state of the given detached document into this UnitOfWork.
1957
     *
1958
     * @param object $document
1959
     * @return object The managed copy of the document.
1960
     */
1961 13
    public function merge($document)
1962
    {
1963 13
        $visited = array();
1964
1965 13
        return $this->doMerge($document, $visited);
1966
    }
1967
1968
    /**
1969
     * Executes a merge operation on a document.
1970
     *
1971
     * @param object      $document
1972
     * @param array       $visited
1973
     * @param object|null $prevManagedCopy
1974
     * @param array|null  $assoc
1975
     *
1976
     * @return object The managed copy of the document.
1977
     *
1978
     * @throws InvalidArgumentException If the entity instance is NEW.
1979
     * @throws LockException If the document uses optimistic locking through a
1980
     *                       version attribute and the version check against the
1981
     *                       managed copy fails.
1982
     */
1983 13
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1984
    {
1985 13
        $oid = spl_object_hash($document);
1986
1987 13
        if (isset($visited[$oid])) {
1988 1
            return $visited[$oid]; // Prevent infinite recursion
1989
        }
1990
1991 13
        $visited[$oid] = $document; // mark visited
1992
1993 13
        $class = $this->dm->getClassMetadata(get_class($document));
1994
1995
        /* First we assume DETACHED, although it can still be NEW but we can
1996
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1997
         * an identity, we need to fetch it from the DB anyway in order to
1998
         * merge. MANAGED documents are ignored by the merge operation.
1999
         */
2000 13
        $managedCopy = $document;
2001
2002 13
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
2003 13
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
2004
                $document->__load();
2005
            }
2006
2007
            // Try to look the document up in the identity map.
2008 13
            $id = $class->isEmbeddedDocument ? null : $class->getIdentifierObject($document);
2009
2010 13
            if ($id === null) {
2011
                // If there is no identifier, it is actually NEW.
2012 5
                $managedCopy = $class->newInstance();
2013 5
                $this->persistNew($class, $managedCopy);
2014 5
            } else {
2015 10
                $managedCopy = $this->tryGetById($id, $class);
2016
2017 10
                if ($managedCopy) {
2018
                    // We have the document in memory already, just make sure it is not removed.
2019 5
                    if ($this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
2020
                        throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
2021
                    }
2022 5
                } else {
2023
                    // We need to fetch the managed copy in order to merge.
2024 7
                    $managedCopy = $this->dm->find($class->name, $id);
2025
                }
2026
2027 10
                if ($managedCopy === null) {
2028
                    // If the identifier is ASSIGNED, it is NEW
2029
                    $managedCopy = $class->newInstance();
2030
                    $class->setIdentifierValue($managedCopy, $id);
2031
                    $this->persistNew($class, $managedCopy);
2032
                } else {
2033 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...
2034
                        $managedCopy->__load();
2035
                    }
2036
                }
2037
            }
2038
2039 13
            if ($class->isVersioned) {
2040
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
2041
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2042
2043
                // Throw exception if versions don't match
2044
                if ($managedCopyVersion != $documentVersion) {
2045
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
2046
                }
2047
            }
2048
2049
            // Merge state of $document into existing (managed) document
2050 13
            foreach ($class->reflClass->getProperties() as $prop) {
2051 13
                $name = $prop->name;
2052 13
                $prop->setAccessible(true);
2053 13
                if ( ! isset($class->associationMappings[$name])) {
2054 13
                    if ( ! $class->isIdentifier($name)) {
2055 13
                        $prop->setValue($managedCopy, $prop->getValue($document));
2056 13
                    }
2057 13
                } else {
2058 13
                    $assoc2 = $class->associationMappings[$name];
2059
2060 13
                    if ($assoc2['type'] === 'one') {
2061 5
                        $other = $prop->getValue($document);
2062
2063 5
                        if ($other === null) {
2064 2
                            $prop->setValue($managedCopy, null);
2065 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...
2066
                            // Do not merge fields marked lazy that have not been fetched
2067 1
                            continue;
2068 3
                        } elseif ( ! $assoc2['isCascadeMerge']) {
2069
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
2070
                                $targetDocument = isset($assoc2['targetDocument']) ? $assoc2['targetDocument'] : get_class($other);
2071
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo */
2072
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
2073
                                $relatedId = $targetClass->getIdentifierObject($other);
2074
2075
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
2076
                                    $other = $this->dm->find($targetClass->name, $relatedId);
2077
                                } else {
2078
                                    $other = $this
2079
                                        ->dm
2080
                                        ->getProxyFactory()
2081
                                        ->getProxy($assoc2['targetDocument'], array($targetClass->identifier => $relatedId));
2082
                                    $this->registerManaged($other, $relatedId, array());
0 ignored issues
show
Documentation introduced by
$relatedId is of type object<MongoId>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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

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

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

Loading history...
2288
    {
2289 16
        $class = $this->dm->getClassMetadata(get_class($document));
2290 16
        foreach ($class->fieldMappings as $mapping) {
2291 16
            if ( ! $mapping['isCascadeDetach']) {
2292 16
                continue;
2293
            }
2294 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2295 11
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2296 11
                if ($relatedDocuments instanceof PersistentCollection) {
2297
                    // Unwrap so that foreach() does not initialize
2298 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2299 8
                }
2300 11
                foreach ($relatedDocuments as $relatedDocument) {
2301 9
                    $this->doDetach($relatedDocument, $visited);
2302 11
                }
2303 11
            } elseif ($relatedDocuments !== null) {
2304 9
                $this->doDetach($relatedDocuments, $visited);
2305 9
            }
2306 16
        }
2307 16
    }
2308
    /**
2309
     * Cascades a merge operation to associated documents.
2310
     *
2311
     * @param object $document
2312
     * @param object $managedCopy
2313
     * @param array $visited
2314
     */
2315 13
    private function cascadeMerge($document, $managedCopy, array &$visited)
2316
    {
2317 13
        $class = $this->dm->getClassMetadata(get_class($document));
2318
2319 13
        $associationMappings = array_filter(
2320 13
            $class->associationMappings,
2321
            function ($assoc) { return $assoc['isCascadeMerge']; }
2322 13
        );
2323
2324 13
        foreach ($associationMappings as $assoc) {
2325 12
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2326
2327 12
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2328 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2329
                    // Collections are the same, so there is nothing to do
2330
                    continue;
2331
                }
2332
2333 8
                if ($relatedDocuments instanceof PersistentCollection) {
2334
                    // Unwrap so that foreach() does not initialize
2335 6
                    $relatedDocuments = $relatedDocuments->unwrap();
2336 6
                }
2337
2338 8
                foreach ($relatedDocuments as $relatedDocument) {
2339 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2340 8
                }
2341 12
            } elseif ($relatedDocuments !== null) {
2342 3
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2343 3
            }
2344 13
        }
2345 13
    }
2346
2347
    /**
2348
     * Cascades the save operation to associated documents.
2349
     *
2350
     * @param object $document
2351
     * @param array $visited
2352
     */
2353 579
    private function cascadePersist($document, array &$visited)
2354
    {
2355 579
        $class = $this->dm->getClassMetadata(get_class($document));
2356
2357 579
        $associationMappings = array_filter(
2358 579
            $class->associationMappings,
2359
            function ($assoc) { return $assoc['isCascadePersist']; }
2360 579
        );
2361
2362 579
        foreach ($associationMappings as $fieldName => $mapping) {
2363 396
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2364
2365 396
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
2366 346
                if ($relatedDocuments instanceof PersistentCollection) {
2367 17
                    if ($relatedDocuments->getOwner() !== $document) {
2368 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2369 2
                    }
2370
                    // Unwrap so that foreach() does not initialize
2371 17
                    $relatedDocuments = $relatedDocuments->unwrap();
2372 17
                }
2373
2374 346
                $count = 0;
2375 346
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2376 193
                    if ( ! empty($mapping['embedded'])) {
2377 119
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2378 119
                        if ($knownParent && $knownParent !== $document) {
2379 4
                            $relatedDocument = clone $relatedDocument;
2380 4
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2381 4
                        }
2382 119
                        $pathKey = ! isset($mapping['strategy']) || CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2383 119
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2384 119
                    }
2385 193
                    $this->doPersist($relatedDocument, $visited);
2386 345
                }
2387 396
            } elseif ($relatedDocuments !== null) {
2388 126
                if ( ! empty($mapping['embedded'])) {
2389 72
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2390 72
                    if ($knownParent && $knownParent !== $document) {
2391 5
                        $relatedDocuments = clone $relatedDocuments;
2392 5
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2393 5
                    }
2394 72
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2395 72
                }
2396 126
                $this->doPersist($relatedDocuments, $visited);
2397 125
            }
2398 578
        }
2399 577
    }
2400
2401
    /**
2402
     * Cascades the delete operation to associated documents.
2403
     *
2404
     * @param object $document
2405
     * @param array $visited
2406
     */
2407 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...
2408
    {
2409 66
        $class = $this->dm->getClassMetadata(get_class($document));
2410 66
        foreach ($class->fieldMappings as $mapping) {
2411 66
            if ( ! $mapping['isCascadeRemove']) {
2412 66
                continue;
2413
            }
2414 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...
2415 2
                $document->__load();
2416 2
            }
2417
2418 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2419 33
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
2420
                // If its a PersistentCollection initialization is intended! No unwrap!
2421 24
                foreach ($relatedDocuments as $relatedDocument) {
2422 13
                    $this->doRemove($relatedDocument, $visited);
2423 24
                }
2424 33
            } elseif ($relatedDocuments !== null) {
2425 12
                $this->doRemove($relatedDocuments, $visited);
2426 12
            }
2427 66
        }
2428 66
    }
2429
2430
    /**
2431
     * Acquire a lock on the given document.
2432
     *
2433
     * @param object $document
2434
     * @param int $lockMode
2435
     * @param int $lockVersion
2436
     * @throws LockException
2437
     * @throws \InvalidArgumentException
2438
     */
2439 9
    public function lock($document, $lockMode, $lockVersion = null)
2440
    {
2441 9
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2442 1
            throw new \InvalidArgumentException("Document is not MANAGED.");
2443
        }
2444
2445 8
        $documentName = get_class($document);
2446 8
        $class = $this->dm->getClassMetadata($documentName);
2447
2448 8
        if ($lockMode == LockMode::OPTIMISTIC) {
2449 3
            if ( ! $class->isVersioned) {
2450 1
                throw LockException::notVersioned($documentName);
2451
            }
2452
2453 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...
2454 2
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2455 2
                if ($documentVersion != $lockVersion) {
2456 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2457
                }
2458 1
            }
2459 6
        } elseif (in_array($lockMode, array(LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE))) {
2460 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2461 5
        }
2462 6
    }
2463
2464
    /**
2465
     * Releases a lock on the given document.
2466
     *
2467
     * @param object $document
2468
     * @throws \InvalidArgumentException
2469
     */
2470 1
    public function unlock($document)
2471
    {
2472 1
        if ($this->getDocumentState($document) != self::STATE_MANAGED) {
2473
            throw new \InvalidArgumentException("Document is not MANAGED.");
2474
        }
2475 1
        $documentName = get_class($document);
2476 1
        $this->getDocumentPersister($documentName)->unlock($document);
2477 1
    }
2478
2479
    /**
2480
     * Clears the UnitOfWork.
2481
     *
2482
     * @param string|null $documentName if given, only documents of this type will get detached.
2483
     */
2484 394
    public function clear($documentName = null)
2485
    {
2486 394
        if ($documentName === null) {
2487 386
            $this->identityMap =
2488 386
            $this->documentIdentifiers =
2489 386
            $this->originalDocumentData =
2490 386
            $this->documentChangeSets =
2491 386
            $this->documentStates =
2492 386
            $this->scheduledForDirtyCheck =
2493 386
            $this->documentInsertions =
2494 386
            $this->documentUpserts =
2495 386
            $this->documentUpdates =
2496 386
            $this->documentDeletions =
2497 386
            $this->collectionUpdates =
2498 386
            $this->collectionDeletions =
2499 386
            $this->parentAssociations =
2500 386
            $this->embeddedDocumentsRegistry =
2501 386
            $this->orphanRemovals =
2502 386
            $this->hasScheduledCollections = array();
2503 386
        } else {
2504 8
            $visited = array();
2505 8
            foreach ($this->identityMap as $className => $documents) {
2506 8
                if ($className === $documentName) {
2507 5
                    foreach ($documents as $document) {
2508 5
                        $this->doDetach($document, $visited);
2509 5
                    }
2510 5
                }
2511 8
            }
2512
        }
2513
2514 394 View Code Duplication
        if ($this->evm->hasListeners(Events::onClear)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2515
            $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2516
        }
2517 394
    }
2518
2519
    /**
2520
     * INTERNAL:
2521
     * Schedules an embedded document for removal. The remove() operation will be
2522
     * invoked on that document at the beginning of the next commit of this
2523
     * UnitOfWork.
2524
     *
2525
     * @ignore
2526
     * @param object $document
2527
     */
2528 47
    public function scheduleOrphanRemoval($document)
2529
    {
2530 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2531 47
    }
2532
2533
    /**
2534
     * INTERNAL:
2535
     * Unschedules an embedded or referenced object for removal.
2536
     *
2537
     * @ignore
2538
     * @param object $document
2539
     */
2540 103
    public function unscheduleOrphanRemoval($document)
2541
    {
2542 103
        $oid = spl_object_hash($document);
2543 103
        if (isset($this->orphanRemovals[$oid])) {
2544 1
            unset($this->orphanRemovals[$oid]);
2545 1
        }
2546 103
    }
2547
2548
    /**
2549
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2550
     *  1) sets owner if it was cloned
2551
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2552
     *  3) NOP if state is OK
2553
     * Returned collection should be used from now on (only important with 2nd point)
2554
     *
2555
     * @param PersistentCollection $coll
2556
     * @param object $document
2557
     * @param ClassMetadata $class
2558
     * @param string $propName
2559
     * @return PersistentCollection
2560
     */
2561 8
    private function fixPersistentCollectionOwnership(PersistentCollection $coll, $document, ClassMetadata $class, $propName)
2562
    {
2563 8
        $owner = $coll->getOwner();
2564 8
        if ($owner === null) { // cloned
2565 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2566 8
        } elseif ($owner !== $document) { // no clone, we have to fix
2567 2
            if ( ! $coll->isInitialized()) {
2568 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2569 1
            }
2570 2
            $newValue = clone $coll;
2571 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2572 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2573 2
            if ($this->isScheduledForUpdate($document)) {
2574
                // @todo following line should be superfluous once collections are stored in change sets
2575
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2576
            }
2577 2
            return $newValue;
2578
        }
2579 6
        return $coll;
2580
    }
2581
2582
    /**
2583
     * INTERNAL:
2584
     * Schedules a complete collection for removal when this UnitOfWork commits.
2585
     *
2586
     * @param PersistentCollection $coll
2587
     */
2588 41
    public function scheduleCollectionDeletion(PersistentCollection $coll)
2589
    {
2590 41
        $oid = spl_object_hash($coll);
2591 41
        unset($this->collectionUpdates[$oid]);
2592 41
        if ( ! isset($this->collectionDeletions[$oid])) {
2593 41
            $this->collectionDeletions[$oid] = $coll;
2594 41
            $this->scheduleCollectionOwner($coll);
2595 41
        }
2596 41
    }
2597
2598
    /**
2599
     * Checks whether a PersistentCollection is scheduled for deletion.
2600
     *
2601
     * @param PersistentCollection $coll
2602
     * @return boolean
2603
     */
2604 108
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
2605
    {
2606 108
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2607
    }
2608
    
2609
    /**
2610
     * INTERNAL:
2611
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2612
     * 
2613
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2614
     */
2615 210 View Code Duplication
    public function unscheduleCollectionDeletion(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2616
    {
2617 210
        $oid = spl_object_hash($coll);
2618 210
        if (isset($this->collectionDeletions[$oid])) {
2619 11
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2620 11
            unset($this->collectionDeletions[$oid]);
2621 11
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2622 11
        }
2623 210
    }
2624
2625
    /**
2626
     * INTERNAL:
2627
     * Schedules a collection for update when this UnitOfWork commits.
2628
     *
2629
     * @param PersistentCollection $coll
2630
     */
2631 227
    public function scheduleCollectionUpdate(PersistentCollection $coll)
2632
    {
2633 227
        $mapping = $coll->getMapping();
2634 227
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2635
            /* There is no need to $unset collection if it will be $set later
2636
             * This is NOP if collection is not scheduled for deletion
2637
             */
2638 40
            $this->unscheduleCollectionDeletion($coll);
2639 40
        }
2640 227
        $oid = spl_object_hash($coll);
2641 227
        if ( ! isset($this->collectionUpdates[$oid])) {
2642 227
            $this->collectionUpdates[$oid] = $coll;
2643 227
            $this->scheduleCollectionOwner($coll);
2644 227
        }
2645 227
    }
2646
    
2647
    /**
2648
     * INTERNAL:
2649
     * Unschedules a collection from being updated when this UnitOfWork commits.
2650
     * 
2651
     * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll
2652
     */
2653 210 View Code Duplication
    public function unscheduleCollectionUpdate(PersistentCollection $coll)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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

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

Loading history...
2654
    {
2655 210
        $oid = spl_object_hash($coll);
2656 210
        if (isset($this->collectionUpdates[$oid])) {
2657 200
            $topmostOwner = $this->getOwningDocument($coll->getOwner());
2658 200
            unset($this->collectionUpdates[$oid]);
2659 200
            unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2660 200
        }
2661 210
    }
2662
    
2663
    /**
2664
     * Checks whether a PersistentCollection is scheduled for update.
2665
     *
2666
     * @param PersistentCollection $coll
2667
     * @return boolean
2668
     */
2669 124
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
2670
    {
2671 124
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2672
    }
2673
2674
    /**
2675
     * INTERNAL:
2676
     * Gets PersistentCollections that have been visited during computing change
2677
     * set of $document
2678
     *
2679
     * @param object $document
2680
     * @return PersistentCollection[]
2681
     */
2682 544
    public function getVisitedCollections($document)
2683
    {
2684 544
        $oid = spl_object_hash($document);
2685 544
        return isset($this->visitedCollections[$oid])
2686 544
                ? $this->visitedCollections[$oid]
2687 544
                : array();
2688
    }
2689
    
2690
    /**
2691
     * INTERNAL:
2692
     * Gets PersistentCollections that are scheduled to update and related to $document
2693
     * 
2694
     * @param object $document
2695
     * @return array
2696
     */
2697 544
    public function getScheduledCollections($document)
2698
    {
2699 544
        $oid = spl_object_hash($document);
2700 544
        return isset($this->hasScheduledCollections[$oid]) 
2701 544
                ? $this->hasScheduledCollections[$oid]
2702 544
                : array();
2703
    }
2704
    
2705
    /**
2706
     * Checks whether the document is related to a PersistentCollection
2707
     * scheduled for update or deletion.
2708
     *
2709
     * @param object $document
2710
     * @return boolean
2711
     */
2712 62
    public function hasScheduledCollections($document)
2713
    {
2714 62
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2715
    }
2716
    
2717
    /**
2718
     * Marks the PersistentCollection's top-level owner as having a relation to
2719
     * a collection scheduled for update or deletion.
2720
     *
2721
     * If the owner is not scheduled for any lifecycle action, it will be
2722
     * scheduled for update to ensure that versioning takes place if necessary.
2723
     *
2724
     * If the collection is nested within atomic collection, it is immediately
2725
     * unscheduled and atomic one is scheduled for update instead. This makes
2726
     * calculating update data way easier.
2727
     * 
2728
     * @param PersistentCollection $coll
2729
     */
2730 229
    private function scheduleCollectionOwner(PersistentCollection $coll)
2731
    {
2732 229
        $document = $this->getOwningDocument($coll->getOwner());
2733 229
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2734
2735 229
        if ($document !== $coll->getOwner()) {
2736 24
            $parent = $coll->getOwner();
2737 24
            while (null !== ($parentAssoc = $this->getParentAssociation($parent))) {
2738 24
                list($mapping, $parent, ) = $parentAssoc;
2739 24
            }
2740 24
            if (isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
2741 7
                $class = $this->dm->getClassMetadata(get_class($document));
2742 7
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2743 7
                $this->scheduleCollectionUpdate($atomicCollection);
2744 7
                $this->unscheduleCollectionDeletion($coll);
2745 7
                $this->unscheduleCollectionUpdate($coll);
2746 7
            }
2747 24
        }
2748
2749 229
        if ( ! $this->isDocumentScheduled($document)) {
2750 94
            $this->scheduleForUpdate($document);
2751 94
        }
2752 229
    }
2753
2754
    /**
2755
     * Get the top-most owning document of a given document
2756
     *
2757
     * If a top-level document is provided, that same document will be returned.
2758
     * For an embedded document, we will walk through parent associations until
2759
     * we find a top-level document.
2760
     *
2761
     * @param object $document
2762
     * @throws \UnexpectedValueException when a top-level document could not be found
2763
     * @return object
2764
     */
2765 231
    public function getOwningDocument($document)
2766
    {
2767 231
        $class = $this->dm->getClassMetadata(get_class($document));
2768 231
        while ($class->isEmbeddedDocument) {
2769 38
            $parentAssociation = $this->getParentAssociation($document);
2770
2771 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...
2772
                throw new \UnexpectedValueException("Could not determine parent association for " . get_class($document));
2773
            }
2774
2775 38
            list(, $document, ) = $parentAssociation;
2776 38
            $class = $this->dm->getClassMetadata(get_class($document));
2777 38
        }
2778
2779 231
        return $document;
2780
    }
2781
2782
    /**
2783
     * Gets the class name for an association (embed or reference) with respect
2784
     * to any discriminator value.
2785
     *
2786
     * @param array      $mapping Field mapping for the association
2787
     * @param array|null $data    Data for the embedded document or reference
2788
     */
2789 208
    public function getClassNameForAssociation(array $mapping, $data)
2790
    {
2791 208
        $discriminatorField = isset($mapping['discriminatorField']) ? $mapping['discriminatorField'] : null;
2792
2793 208
        $discriminatorValue = null;
2794 208
        if (isset($discriminatorField, $data[$discriminatorField])) {
2795 21
            $discriminatorValue = $data[$discriminatorField];
2796 208
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2797
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2798
        }
2799
2800 208
        if ($discriminatorValue !== null) {
2801 21
            return isset($mapping['discriminatorMap'][$discriminatorValue])
2802 21
                ? $mapping['discriminatorMap'][$discriminatorValue]
2803 21
                : $discriminatorValue;
2804
        }
2805
2806 188
            $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2807
2808 188 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2809 15
            $discriminatorValue = $data[$class->discriminatorField];
2810 188
        } elseif ($class->defaultDiscriminatorValue !== null) {
2811 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2812 1
        }
2813
2814 188
        if ($discriminatorValue !== null) {
2815 16
            return isset($class->discriminatorMap[$discriminatorValue])
2816 16
                ? $class->discriminatorMap[$discriminatorValue]
2817 16
                : $discriminatorValue;
2818
        }
2819
2820 172
        return $mapping['targetDocument'];
2821
    }
2822
2823
    /**
2824
     * INTERNAL:
2825
     * Creates a document. Used for reconstitution of documents during hydration.
2826
     *
2827
     * @ignore
2828
     * @param string $className The name of the document class.
2829
     * @param array $data The data for the document.
2830
     * @param array $hints Any hints to account for during reconstitution/lookup of the document.
2831
     * @param object The document to be hydrated into in case of creation
2832
     * @return object The document instance.
2833
     * @internal Highly performance-sensitive method.
2834
     */
2835 387
    public function getOrCreateDocument($className, $data, &$hints = array(), $document = null)
2836
    {
2837 387
        $class = $this->dm->getClassMetadata($className);
2838
2839
        // @TODO figure out how to remove this
2840 387
        $discriminatorValue = null;
2841 387 View Code Duplication
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2842 19
            $discriminatorValue = $data[$class->discriminatorField];
2843 387
        } elseif (isset($class->defaultDiscriminatorValue)) {
2844 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2845 2
        }
2846
2847 387
        if ($discriminatorValue !== null) {
2848 20
            $className = isset($class->discriminatorMap[$discriminatorValue])
2849 20
                ? $class->discriminatorMap[$discriminatorValue]
2850 20
                : $discriminatorValue;
2851
2852 20
            $class = $this->dm->getClassMetadata($className);
2853
2854 20
            unset($data[$class->discriminatorField]);
2855 20
        }
2856
2857 387
        $id = $class->getDatabaseIdentifierValue($data['_id']);
2858 387
        $serializedId = serialize($id);
2859
2860 387
        if (isset($this->identityMap[$class->name][$serializedId])) {
2861 92
            $document = $this->identityMap[$class->name][$serializedId];
2862 92
            $oid = spl_object_hash($document);
2863 92
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

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

Available Fixes

  1. Adding an additional type check:

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

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