Completed
Pull Request — master (#1790)
by Andreas
20:58
created

UnitOfWork   F

Complexity

Total Complexity 459

Size/Duplication

Total Lines 2894
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 20

Test Coverage

Coverage 91.32%

Importance

Changes 0
Metric Value
wmc 459
lcom 1
cbo 20
dl 0
loc 2894
ccs 1010
cts 1106
cp 0.9132
rs 0.6314
c 0
b 0
f 0

96 Methods

Rating   Name   Duplication   Size   Complexity  
A objToStr() 0 4 2
A __construct() 0 7 1
A getPersistenceBuilder() 0 7 2
A setParentAssociation() 0 6 1
A getParentAssociation() 0 6 1
A getDocumentPersister() 0 9 2
A getCollectionPersister() 0 8 2
A setDocumentPersister() 0 4 1
F commit() 0 78 18
C getClassesForCommitAction() 0 29 8
A computeScheduleInsertsChangeSets() 0 11 3
A computeScheduleUpsertsChangeSets() 0 11 3
A getDocumentChangeSet() 0 6 1
A setDocumentChangeSet() 0 4 1
D getDocumentActualData() 0 30 9
A computeChangeSet() 0 13 3
F computeOrRecomputeChangeSet() 0 190 57
C computeChangeSets() 0 51 12
F computeAssociationChanges() 0 104 30
B recomputeSingleDocumentChangeSet() 0 19 6
C persistNew() 0 43 11
A executeInserts() 0 15 3
A executeUpserts() 0 15 3
B executeUpdates() 0 21 5
C executeDeletions() 0 36 7
B scheduleForInsert() 0 22 5
B scheduleForUpsert() 0 21 5
A isScheduledForInsert() 0 4 1
A isScheduledForUpsert() 0 4 1
B scheduleForUpdate() 0 19 6
A isScheduledForUpdate() 0 4 1
A isScheduledForDirtyCheck() 0 5 1
B scheduleForDelete() 0 28 6
A isScheduledForDelete() 0 4 1
A isDocumentScheduled() 0 8 4
B addToIdentityMap() 0 18 5
C getDocumentState() 0 50 9
A removeFromIdentityMap() 0 20 3
A getById() 0 10 2
A tryGetById() 0 10 2
A scheduleForDirtyCheck() 0 5 1
A isInIdentityMap() 0 13 2
A getIdForIdentityMap() 0 13 2
A containsId() 0 4 1
A persist() 0 9 3
C doPersist() 0 45 8
A remove() 0 5 1
B doRemove() 0 32 6
A merge() 0 6 1
F doMerge() 0 176 41
A detach() 0 5 1
B doDetach() 0 32 5
A refresh() 0 5 1
A doRefresh() 0 21 4
C cascadeRefresh() 0 26 7
B cascadeDetach() 0 21 8
C cascadeMerge() 0 28 7
C cascadePersist() 0 49 15
C lock() 0 24 7
A unlock() 0 8 2
B clear() 0 38 6
A scheduleOrphanRemoval() 0 4 1
A unscheduleOrphanRemoval() 0 5 1
B fixPersistentCollectionOwnership() 0 20 5
A scheduleCollectionDeletion() 0 11 2
A isCollectionScheduledForDeletion() 0 4 1
A unscheduleCollectionDeletion() 0 11 2
A scheduleCollectionUpdate() 0 17 3
A unscheduleCollectionUpdate() 0 11 2
A isCollectionScheduledForUpdate() 0 4 1
A getVisitedCollections() 0 6 1
A getScheduledCollections() 0 6 1
A hasScheduledCollections() 0 4 1
B scheduleCollectionOwner() 0 26 5
A getOwningDocument() 0 16 3
C getClassNameForAssociation() 0 30 7
C getOrCreateDocument() 0 73 14
A loadCollection() 0 5 1
A getIdentityMap() 0 4 1
A getOriginalDocumentData() 0 6 1
A setOriginalDocumentData() 0 6 1
A setOriginalDocumentProperty() 0 4 1
A getDocumentIdentifier() 0 4 1
A hasPendingInsertions() 0 4 1
A size() 0 8 2
A registerManaged() 0 15 3
A clearDocumentChangeSet() 0 4 1
A propertyChanged() 0 17 3
A getScheduledDocumentInsertions() 0 4 1
A getScheduledDocumentUpserts() 0 4 1
A getScheduledDocumentUpdates() 0 4 1
A getScheduledDocumentDeletions() 0 4 1
A getScheduledCollectionDeletions() 0 4 1
A getScheduledCollectionUpdates() 0 4 1
A initializeObject() 0 8 3
C cascadeRemove() 0 22 11

How to fix   Complexity   

Complex Class

Complex classes like UnitOfWork often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UnitOfWork, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
16
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
17
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
18
use Doctrine\ODM\MongoDB\Proxy\Proxy;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use MongoDB\BSON\UTCDateTime;
25
use function array_filter;
26
use function count;
27
use function get_class;
28
use function in_array;
29
use function is_array;
30
use function is_object;
31
use function method_exists;
32
use function preg_match;
33
use function serialize;
34
use function spl_object_hash;
35
use function sprintf;
36
37
/**
38
 * The UnitOfWork is responsible for tracking changes to objects during an
39
 * "object-level" transaction and for writing out changes to the database
40
 * in the correct order.
41
 *
42
 */
43
class UnitOfWork implements PropertyChangedListener
44
{
45
    /**
46
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
47
     */
48
    public const STATE_MANAGED = 1;
49
50
    /**
51
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
52
     * and is not (yet) managed by a DocumentManager.
53
     */
54
    public const STATE_NEW = 2;
55
56
    /**
57
     * A detached document is an instance with a persistent identity that is not
58
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
59
     */
60
    public const STATE_DETACHED = 3;
61
62
    /**
63
     * A removed document instance is an instance with a persistent identity,
64
     * associated with a DocumentManager, whose persistent state has been
65
     * deleted (or is scheduled for deletion).
66
     */
67
    public const STATE_REMOVED = 4;
68
69
    /**
70
     * The identity map holds references to all managed documents.
71
     *
72
     * Documents are grouped by their class name, and then indexed by the
73
     * serialized string of their database identifier field or, if the class
74
     * has no identifier, the SPL object hash. Serializing the identifier allows
75
     * differentiation of values that may be equal (via type juggling) but not
76
     * identical.
77
     *
78
     * Since all classes in a hierarchy must share the same identifier set,
79
     * we always take the root class name of the hierarchy.
80
     *
81
     * @var array
82
     */
83
    private $identityMap = [];
84
85
    /**
86
     * Map of all identifiers of managed documents.
87
     * Keys are object ids (spl_object_hash).
88
     *
89
     * @var array
90
     */
91
    private $documentIdentifiers = [];
92
93
    /**
94
     * Map of the original document data of managed documents.
95
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
96
     * at commit time.
97
     *
98
     * @var array
99
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
100
     *           A value will only really be copied if the value in the document is modified
101
     *           by the user.
102
     */
103
    private $originalDocumentData = [];
104
105
    /**
106
     * Map of document changes. Keys are object ids (spl_object_hash).
107
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
108
     *
109
     * @var array
110
     */
111
    private $documentChangeSets = [];
112
113
    /**
114
     * The (cached) states of any known documents.
115
     * Keys are object ids (spl_object_hash).
116
     *
117
     * @var array
118
     */
119
    private $documentStates = [];
120
121
    /**
122
     * Map of documents that are scheduled for dirty checking at commit time.
123
     *
124
     * Documents are grouped by their class name, and then indexed by their SPL
125
     * object hash. This is only used for documents with a change tracking
126
     * policy of DEFERRED_EXPLICIT.
127
     *
128
     * @var array
129
     * @todo rename: scheduledForSynchronization
130
     */
131
    private $scheduledForDirtyCheck = [];
132
133
    /**
134
     * A list of all pending document insertions.
135
     *
136
     * @var array
137
     */
138
    private $documentInsertions = [];
139
140
    /**
141
     * A list of all pending document updates.
142
     *
143
     * @var array
144
     */
145
    private $documentUpdates = [];
146
147
    /**
148
     * A list of all pending document upserts.
149
     *
150
     * @var array
151
     */
152
    private $documentUpserts = [];
153
154
    /**
155
     * A list of all pending document deletions.
156
     *
157
     * @var array
158
     */
159
    private $documentDeletions = [];
160
161
    /**
162
     * All pending collection deletions.
163
     *
164
     * @var array
165
     */
166
    private $collectionDeletions = [];
167
168
    /**
169
     * All pending collection updates.
170
     *
171
     * @var array
172
     */
173
    private $collectionUpdates = [];
174
175
    /**
176
     * A list of documents related to collections scheduled for update or deletion
177
     *
178
     * @var array
179
     */
180
    private $hasScheduledCollections = [];
181
182
    /**
183
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
184
     * At the end of the UnitOfWork all these collections will make new snapshots
185
     * of their data.
186
     *
187
     * @var array
188
     */
189
    private $visitedCollections = [];
190
191
    /**
192
     * The DocumentManager that "owns" this UnitOfWork instance.
193
     *
194
     * @var DocumentManager
195
     */
196
    private $dm;
197
198
    /**
199
     * The EventManager used for dispatching events.
200
     *
201
     * @var EventManager
202
     */
203
    private $evm;
204
205
    /**
206
     * Additional documents that are scheduled for removal.
207
     *
208
     * @var array
209
     */
210
    private $orphanRemovals = [];
211
212
    /**
213
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
214
     *
215
     * @var HydratorFactory
216
     */
217
    private $hydratorFactory;
218
219
    /**
220
     * The document persister instances used to persist document instances.
221
     *
222
     * @var array
223
     */
224
    private $persisters = [];
225
226
    /**
227
     * The collection persister instance used to persist changes to collections.
228
     *
229
     * @var Persisters\CollectionPersister
230
     */
231
    private $collectionPersister;
232
233
    /**
234
     * The persistence builder instance used in DocumentPersisters.
235
     *
236
     * @var PersistenceBuilder
237
     */
238
    private $persistenceBuilder;
239
240
    /**
241
     * Array of parent associations between embedded documents.
242
     *
243
     * @var array
244
     */
245
    private $parentAssociations = [];
246
247
    /** @var LifecycleEventManager */
248
    private $lifecycleEventManager;
249
250
    /**
251
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
252
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
253
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
254
     *
255
     * @var array
256
     */
257
    private $embeddedDocumentsRegistry = [];
258
259
    /** @var int */
260
    private $commitsInProgress = 0;
261
262
    /**
263
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
264
     *
265 1584
     */
266
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
267 1584
    {
268 1584
        $this->dm = $dm;
269 1584
        $this->evm = $evm;
270 1584
        $this->hydratorFactory = $hydratorFactory;
271 1584
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
272
    }
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 1080
     */
280
    public function getPersistenceBuilder()
281 1080
    {
282 1080
        if (! $this->persistenceBuilder) {
283
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
284 1080
        }
285
        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 178
     */
296
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
297 178
    {
298 178
        $oid = spl_object_hash($document);
299 178
        $this->embeddedDocumentsRegistry[$oid] = $document;
300 178
        $this->parentAssociations[$oid] = [$mapping, $parent, $propertyPath];
301
    }
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 205
     */
313
    public function getParentAssociation($document)
314 205
    {
315
        $oid = spl_object_hash($document);
316 205
317
        return $this->parentAssociations[$oid] ?? null;
318
    }
319
320
    /**
321
     * Get the document persister instance for the given document name
322
     *
323
     * @param string $documentName
324
     * @return Persisters\DocumentPersister
325 1078
     */
326
    public function getDocumentPersister($documentName)
327 1078
    {
328 1065
        if (! isset($this->persisters[$documentName])) {
329 1065
            $class = $this->dm->getClassMetadata($documentName);
330 1065
            $pb = $this->getPersistenceBuilder();
331
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
332 1078
        }
333
        return $this->persisters[$documentName];
334
    }
335
336
    /**
337
     * Get the collection persister instance.
338
     *
339
     * @return CollectionPersister
340 1078
     */
341
    public function getCollectionPersister()
342 1078
    {
343 1078
        if (! isset($this->collectionPersister)) {
344 1078
            $pb = $this->getPersistenceBuilder();
345
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
346 1078
        }
347
        return $this->collectionPersister;
348
    }
349
350
    /**
351
     * Set the document persister instance to use for the given document name
352
     *
353
     * @param string $documentName
354 13
     */
355
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
356 13
    {
357 13
        $this->persisters[$documentName] = $persister;
358
    }
359
360
    /**
361
     * Commits the UnitOfWork, executing all operations that have been postponed
362
     * up to this point. The state of all managed documents will be synchronized with
363
     * the database.
364
     *
365
     * The operations are executed in the following order:
366
     *
367
     * 1) All document insertions
368
     * 2) All document updates
369
     * 3) All document deletions
370
     *
371
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
372 569
     */
373
    public function commit(array $options = [])
374
    {
375 569
        // Raise preFlush
376
        if ($this->evm->hasListeners(Events::preFlush)) {
377
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
378
        }
379
380 569
        // Compute changes done since last commit.
381
        $this->computeChangeSets();
382 568
383 242
        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...
384 201
            $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...
385 186
            $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...
386 22
            $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...
387 22
            $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...
388 568
            $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...
389
            $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...
390 22
        ) {
391
            return; // Nothing to do.
392
        }
393 565
394 565
        $this->commitsInProgress++;
395
        if ($this->commitsInProgress > 1) {
396
            throw MongoDBException::commitInProgress();
397
        }
398 565
        try {
399 44
            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...
400 44
                foreach ($this->orphanRemovals as $removal) {
401
                    $this->remove($removal);
402
                }
403
            }
404
405 565
            // Raise onFlush
406 4
            if ($this->evm->hasListeners(Events::onFlush)) {
407
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
408
            }
409 564
410 84
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
411 84
                list($class, $documents) = $classAndDocuments;
412
                $this->executeUpserts($class, $documents, $options);
413
            }
414 564
415 491
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
416 491
                list($class, $documents) = $classAndDocuments;
417
                $this->executeInserts($class, $documents, $options);
418
            }
419 563
420 205
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
421 205
                list($class, $documents) = $classAndDocuments;
422
                $this->executeUpdates($class, $documents, $options);
423
            }
424 563
425 66
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
426 66
                list($class, $documents) = $classAndDocuments;
427
                $this->executeDeletions($class, $documents, $options);
428
            }
429
430 563
            // Raise postFlush
431
            if ($this->evm->hasListeners(Events::postFlush)) {
432
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
433
            }
434
435 563
            // Clear up
436 563
            $this->documentInsertions =
437 563
            $this->documentUpserts =
438 563
            $this->documentUpdates =
439 563
            $this->documentDeletions =
440 563
            $this->documentChangeSets =
441 563
            $this->collectionUpdates =
442 563
            $this->collectionDeletions =
443 563
            $this->visitedCollections =
444 563
            $this->scheduledForDirtyCheck =
445 563
            $this->orphanRemovals =
446 563
            $this->hasScheduledCollections = [];
447 565
        } finally {
448
            $this->commitsInProgress--;
449 563
        }
450
    }
451
452
    /**
453
     * Groups a list of scheduled documents by their class.
454
     *
455
     * @param array $documents       Scheduled documents (e.g. $this->documentInsertions)
456
     * @param bool  $includeEmbedded
457
     * @return array Tuples of ClassMetadata and a corresponding array of objects
458 564
     */
459
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
460 564
    {
461 564
        if (empty($documents)) {
462
            return [];
463 563
        }
464 563
        $divided = [];
465 563
        $embeds = [];
466 563
        foreach ($documents as $oid => $d) {
467 563
            $className = get_class($d);
468 69
            if (isset($embeds[$className])) {
469
                continue;
470 563
            }
471 156
            if (isset($divided[$className])) {
472 156
                $divided[$className][1][$oid] = $d;
473
                continue;
474 563
            }
475 563
            $class = $this->dm->getClassMetadata($className);
476 155
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
477 155
                $embeds[$className] = true;
478
                continue;
479 563
            }
480 563
            if (empty($divided[$class->name])) {
481
                $divided[$class->name] = [$class, [$oid => $d]];
482 563
            } else {
483
                $divided[$class->name][1][$oid] = $d;
484
            }
485 563
        }
486
        return $divided;
487
    }
488
489
    /**
490
     * Compute changesets of all documents scheduled for insertion.
491
     *
492
     * Embedded documents will not be processed.
493 572
     */
494
    private function computeScheduleInsertsChangeSets()
495 572
    {
496 502
        foreach ($this->documentInsertions as $document) {
497 502
            $class = $this->dm->getClassMetadata(get_class($document));
498 139
            if ($class->isEmbeddedDocument) {
499
                continue;
500
            }
501 497
502
            $this->computeChangeSet($class, $document);
503 571
        }
504
    }
505
506
    /**
507
     * Compute changesets of all documents scheduled for upsert.
508
     *
509
     * Embedded documents will not be processed.
510 571
     */
511
    private function computeScheduleUpsertsChangeSets()
512 571
    {
513 83
        foreach ($this->documentUpserts as $document) {
514 83
            $class = $this->dm->getClassMetadata(get_class($document));
515
            if ($class->isEmbeddedDocument) {
516
                continue;
517
            }
518 83
519
            $this->computeChangeSet($class, $document);
520 571
        }
521
    }
522
523
    /**
524
     * Gets the changeset for a document.
525
     *
526
     * @param object $document
527
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
528 566
     */
529
    public function getDocumentChangeSet($document)
530 566
    {
531
        $oid = spl_object_hash($document);
532 566
533
        return $this->documentChangeSets[$oid] ?? [];
534
    }
535
536
    /**
537
     * INTERNAL:
538
     * Sets the changeset for a document.
539
     *
540
     * @param object $document
541
     * @param array  $changeset
542 1
     */
543
    public function setDocumentChangeSet($document, $changeset)
544 1
    {
545 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
546
    }
547
548
    /**
549
     * Get a documents actual data, flattening all the objects to arrays.
550
     *
551
     * @param object $document
552
     * @return array
553 573
     */
554
    public function getDocumentActualData($document)
555 573
    {
556 573
        $class = $this->dm->getClassMetadata(get_class($document));
557 573
        $actualData = [];
558 573
        foreach ($class->reflFields as $name => $refProp) {
559
            $mapping = $class->fieldMappings[$name];
560 573
            // skip not saved fields
561 27
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
562
                continue;
563 573
            }
564 573
            $value = $refProp->getValue($document);
565 573
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
566
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
567 369
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
568 143
                if (! $value instanceof Collection) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
569
                    $value = new ArrayCollection($value);
570
                }
571
572 369
                // Inject PersistentCollection
573 369
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
574 369
                $coll->setOwner($document, $mapping);
575 369
                $coll->setDirty(! $value->isEmpty());
576 369
                $class->reflFields[$name]->setValue($document, $coll);
577
                $actualData[$name] = $coll;
578 573
            } else {
579
                $actualData[$name] = $value;
580
            }
581 573
        }
582
        return $actualData;
583
    }
584
585
    /**
586
     * Computes the changes that happened to a single document.
587
     *
588
     * Modifies/populates the following properties:
589
     *
590
     * {@link originalDocumentData}
591
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
592
     * then it was not fetched from the database and therefore we have no original
593
     * document data yet. All of the current document data is stored as the original document data.
594
     *
595
     * {@link documentChangeSets}
596
     * The changes detected on all properties of the document are stored there.
597
     * A change is a tuple array where the first entry is the old value and the second
598
     * entry is the new value of the property. Changesets are used by persisters
599
     * to INSERT/UPDATE the persistent document state.
600
     *
601
     * {@link documentUpdates}
602
     * If the document is already fully MANAGED (has been fetched from the database before)
603
     * and any changes to its properties are detected, then a reference to the document is stored
604
     * there to mark it for an update.
605
     *
606
     * @param ClassMetadata $class    The class descriptor of the document.
607
     * @param object        $document The document for which to compute the changes.
608 569
     */
609
    public function computeChangeSet(ClassMetadata $class, $document)
610 569
    {
611 175
        if (! $class->isInheritanceTypeNone()) {
612
            $class = $this->dm->getClassMetadata(get_class($document));
613
        }
614
615 569
        // Fire PreFlush lifecycle callbacks
616 11
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
617
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
618
        }
619 569
620 568
        $this->computeOrRecomputeChangeSet($class, $document);
621
    }
622
623
    /**
624
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
625
     *
626
     * @param object $document
627
     * @param bool   $recompute
628 569
     */
629
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
630 569
    {
631 569
        $oid = spl_object_hash($document);
632 569
        $actualData = $this->getDocumentActualData($document);
633 569
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
634
        if ($isNewDocument) {
635
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
636 569
            // These result in an INSERT.
637 569
            $this->originalDocumentData[$oid] = $actualData;
638 569
            $changeSet = [];
639
            foreach ($actualData as $propName => $actualValue) {
640
                /* At this PersistentCollection shouldn't be here, probably it
641
                 * was cloned and its ownership must be fixed
642 569
                 */
643
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
644
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
645
                    $actualValue = $actualData[$propName];
646
                }
647 569
                // ignore inverse side of reference relationship
648 180
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
649
                    continue;
650 569
                }
651
                $changeSet[$propName] = [null, $actualValue];
652 569
            }
653
            $this->documentChangeSets[$oid] = $changeSet;
654 260
        } else {
655 2
            if ($class->isReadOnly) {
656
                return;
657
            }
658
            // Document is "fully" MANAGED: it was already fully persisted before
659 258
            // and we have a copy of the original data
660 258
            $originalData = $this->originalDocumentData[$oid];
661 258
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
662 2
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
663
                $changeSet = $this->documentChangeSets[$oid];
664 258
            } else {
665
                $changeSet = [];
666
            }
667 258
668
            $gridFSMetadataProperty = null;
669 258
670
            if ($class->isFile) {
671
                try {
672
                    $gridFSMetadata = $class->getFieldMappingByDbFieldName('metadata');
673 258
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
674
                } catch (MappingException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
675
                }
676 258
            }
677 257
678 257
            foreach ($actualData as $propName => $actualValue) {
679
                // skip not saved fields
680
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
681 178
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
682
                    continue;
683 154
                }
684
685
                $orgValue = $originalData[$propName] ?? null;
686
687
                // skip if value has not changed
688 221
                if ($orgValue === $actualValue) {
689 13
                    if (! $actualValue instanceof PersistentCollectionInterface) {
690 8
                        continue;
691
                    }
692 13
693 13
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
694
                        // consider dirty collections as changed as well
695
                        continue;
696
                    }
697 215
                }
698 12
699 1
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
700
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
701
                    if ($orgValue !== null) {
702 12
                        $this->scheduleOrphanRemoval($orgValue);
703 12
                    }
704
                    $changeSet[$propName] = [$orgValue, $actualValue];
705
                    continue;
706 209
                }
707 3
708
                // if owning side of reference-one relationship
709
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
710
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
711 207
                        $this->scheduleOrphanRemoval($orgValue);
712 6
                    }
713
714
                    $changeSet[$propName] = [$orgValue, $actualValue];
715
                    continue;
716
                }
717
718 205
                if ($isChangeTrackingNotify) {
719 6
                    continue;
720
                }
721
722
                // ignore inverse side of reference relationship
723 205
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
724 101
                    continue;
725
                }
726
727
                // Persistent collection was exchanged with the "originally"
728 101
                // created one. This can only mean it was cloned and replaced
729 19
                // on another document.
730
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
731 88
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
732 15
                }
733
734 88
                // if embed-many or reference-many relationship
735
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
736
                    $changeSet[$propName] = [$orgValue, $actualValue];
737
                    /* If original collection was exchanged with a non-empty value
738 133
                     * and $set will be issued, there is no need to $unset it first
739
                     */
740 37
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
741 37
                        continue;
742 37
                    }
743
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
744 37
                        $this->scheduleCollectionDeletion($orgValue);
745 37
                    }
746
                    continue;
747 37
                }
748 30
749
                // skip equivalent date values
750
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
751
                    /** @var DateType $dateType */
752
                    $dateType = Type::getType('date');
753 116
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
754
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
755 258
756 210
                    $orgTimestamp = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
757 16
                    $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
758 208
759
                    if ($orgTimestamp === $actualTimestamp) {
760 210
                        continue;
761 210
                    }
762
                }
763
764
                // regular field
765
                $changeSet[$propName] = [$orgValue, $actualValue];
766 569
            }
767 569
            if ($changeSet) {
768
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
769 432
                    ? $changeSet + $this->documentChangeSets[$oid]
770 569
                    : $changeSet;
771
772
                $this->originalDocumentData[$oid] = $actualData;
773 569
                $this->scheduleForUpdate($document);
774 432
            }
775
        }
776 432
777 295
        // Look for changes in associations of the document
778
        $associationMappings = array_filter(
779
            $class->associationMappings,
780 421
            function ($assoc) {
781
                return empty($assoc['notSaved']);
782 420
            }
783 317
        );
784
785
        foreach ($associationMappings as $mapping) {
786 322
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
787
788 322
            if ($value === null) {
789 159
                continue;
790
            }
791 159
792 157
            $this->computeAssociationChanges($document, $mapping, $value);
793
794
            if (isset($mapping['reference'])) {
795 34
                continue;
796
            }
797
798 157
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
799 65
800
            foreach ($values as $obj) {
801
                $oid2 = spl_object_hash($obj);
802 322
803
                if (isset($this->documentChangeSets[$oid2])) {
804
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
805
                        // instance of $value is the same as it was previously otherwise there would be
806 568
                        // change set already in place
807
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
808
                    }
809
810
                    if (! $isNewDocument) {
811
                        $this->scheduleForUpdate($document);
812
                    }
813 572
814
                    break;
815 572
                }
816 571
            }
817
        }
818
    }
819 571
820 571
    /**
821 571
     * Computes all the changes that have been done to documents and collections
822
     * since the last commit and stores these changes in the _documentChangeSet map
823
     * temporarily for access by the persisters, until the UoW commit is finished.
824
     */
825
    public function computeChangeSets()
826
    {
827 151
        $this->computeScheduleInsertsChangeSets();
828
        $this->computeScheduleUpsertsChangeSets();
829
830
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
831
        foreach ($this->identityMap as $className => $documents) {
832
            $class = $this->dm->getClassMetadata($className);
833 571
            if ($class->isEmbeddedDocument) {
834 570
                /* we do not want to compute changes to embedded documents up front
835 570
                 * in case embedded document was replaced and its changeset
836
                 * would corrupt data. Embedded documents' change set will
837 4
                 * be calculated by reachability from owning document.
838 3
                 */
839 3
                continue;
840
            }
841
842 4
            // If change tracking is explicit or happens through notification, then only compute
843
            // changes on document of that type that are explicitly marked for synchronization.
844
            switch (true) {
845 571
                case ($class->isChangeTrackingDeferredImplicit()):
846
                    $documentsToProcess = $documents;
847 567
                    break;
848 9
849
                case (isset($this->scheduledForDirtyCheck[$className])):
850
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
851 567
                    break;
852 567
853 300
                default:
854 256
                    $documentsToProcess = [];
855 567
            }
856
857 567
            foreach ($documentsToProcess as $document) {
858
                // Ignore uninitialized proxy objects
859
                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...
860 571
                    continue;
861
                }
862
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
863 571
                $oid = spl_object_hash($document);
864
                if (isset($this->documentInsertions[$oid])
865
                    || isset($this->documentUpserts[$oid])
866
                    || isset($this->documentDeletions[$oid])
867
                    || ! isset($this->documentStates[$oid])
868
                ) {
869
                    continue;
870
                }
871
872
                $this->computeChangeSet($class, $document);
873 421
            }
874
        }
875 421
    }
876 421
877 421
    /**
878
     * Computes the changes of an association.
879 421
     *
880 7
     * @param object $parentDocument
881
     * @param array  $assoc
882
     * @param mixed  $value          The value of the association.
883 420
     * @throws \InvalidArgumentException
884 230
     */
885 226
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
886
    {
887 230
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
888 230
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
889 230
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
890 124
891 124
        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...
892 20
            return;
893
        }
894
895
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
896
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
897
                $this->scheduleCollectionUpdate($value);
898
            }
899
            $topmostOwner = $this->getOwningDocument($value->getOwner());
900 420
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
901
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
902 420
                $value->initialize();
903 420
                foreach ($value->getDeletedDocuments() as $orphan) {
904 338
                    $this->scheduleOrphanRemoval($orphan);
905 1
                }
906 1
            }
907
        }
908
909
        // Look through the documents, and in any of their associations,
910 337
        // for transient (new) documents, recursively. ("Persistence by reachability")
911
        // Unwrap. Uninitialized collections will simply be empty.
912 337
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap();
913
914
        $count = 0;
915 337
        foreach ($unwrappedValue as $key => $entry) {
916 337
            if (! is_object($entry)) {
917
                throw new \InvalidArgumentException(
918 337
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
919
                );
920
            }
921 337
922 53
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
923
924
            $state = $this->getDocumentState($entry, self::STATE_NEW);
925
926
            // Handle "set" strategy for multi-level hierarchy
927
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
928
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
929 53
930 53
            $count++;
931 53
932 53
            switch ($state) {
933
                case self::STATE_NEW:
934 333
                    if (! $assoc['isCascadePersist']) {
935 333
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
936 151
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
937 151
                            . ' Explicitly persist the new document or configure cascading persist operations'
938 6
                            . ' on the relationship.');
939 6
                    }
940 3
941 3
                    $this->persistNew($targetClass, $entry);
942 3
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
943 3
                    $this->computeChangeSet($targetClass, $entry);
944 3
                    break;
945
946
                case self::STATE_MANAGED:
947
                    if ($targetClass->isEmbeddedDocument) {
948 4
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
949
                        if ($knownParent && $knownParent !== $parentDocument) {
950 6
                            $entry = clone $entry;
951
                            if ($assoc['type'] === ClassMetadata::ONE) {
952 151
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
953 151
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
954
                                $poid = spl_object_hash($parentDocument);
955 333
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
956
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
957 1
                                }
958
                            } else {
959
                                // must use unwrapped value to not trigger orphan removal
960 1
                                $unwrappedValue[$key] = $entry;
961
                            }
962
                            $this->persistNew($targetClass, $entry);
963 1
                        }
964
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
965
                        $this->computeChangeSet($targetClass, $entry);
966
                    }
967
                    break;
968
969
                case self::STATE_REMOVED:
970
                    // Consume the $value as array (it's either an array or an ArrayAccess)
971 337
                    // and remove the element from Collection.
972
                    if ($assoc['type'] === ClassMetadata::MANY) {
973
                        unset($value[$key]);
974
                    }
975
                    break;
976 419
977
                case self::STATE_DETACHED:
978
                    // Can actually not happen right now as we assume STATE_NEW,
979
                    // so the exception will be raised from the DBAL layer (constraint violation).
980
                    throw new \InvalidArgumentException('A detached document was found through a '
981
                        . 'relationship during cascading a persist operation.');
982
983
                default:
984
                    // MANAGED associated documents are already taken into account
985
                    // during changeset calculation anyway, since they are in the identity map.
986
            }
987
        }
988
    }
989
990
    /**
991
     * INTERNAL:
992 17
     * Computes the changeset of an individual document, independently of the
993
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
994
     *
995 17
     * The passed document must be a managed document. If the document already has a change set
996 1
     * because this method is invoked during a commit cycle then the change sets are added.
997
     * whereby changes detected in this method prevail.
998
     *
999 16
     * @ignore
1000
     * @param ClassMetadata $class    The class descriptor of the document.
1001 16
     * @param object        $document The document for which to (re)calculate the change set.
1002
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1003
     */
1004
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1005 16
    {
1006 1
        // Ignore uninitialized proxy objects
1007
        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...
1008
            return;
1009 16
        }
1010 16
1011
        $oid = spl_object_hash($document);
1012
1013
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
1014
            throw new \InvalidArgumentException('Document must be managed.');
1015
        }
1016 599
1017
        if (! $class->isInheritanceTypeNone()) {
1018 599
            $class = $this->dm->getClassMetadata(get_class($document));
1019 599
        }
1020 599
1021 599
        $this->computeOrRecomputeChangeSet($class, $document, true);
1022 599
    }
1023 599
1024
    /**
1025 599
     * @param object $document
1026 3
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1027 3
     */
1028 3
    private function persistNew(ClassMetadata $class, $document)
1029
    {
1030
        $this->lifecycleEventManager->prePersist($class, $document);
1031
        $oid = spl_object_hash($document);
1032 598
        $upsert = false;
1033 1
        if ($class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1034 1
            $idValue = $class->getIdentifierValue($document);
1035 1
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1036
1037
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1038
                throw new \InvalidArgumentException(sprintf(
1039 597
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1040 520
                    get_class($document)
1041 520
                ));
1042 520
            }
1043
1044
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1045 597
                throw new \InvalidArgumentException(sprintf(
1046
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1047
                    get_class($document)
1048 131
                ));
1049
            }
1050
1051 597
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1052
                $idValue = $class->idGenerator->generate($this->dm, $document);
1053 597
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1054 87
                $class->setIdentifierValue($document, $idValue);
1055
            }
1056 528
1057
            $this->documentIdentifiers[$oid] = $idValue;
1058 597
        } else {
1059
            // this is for embedded documents without identifiers
1060
            $this->documentIdentifiers[$oid] = $oid;
1061
        }
1062
1063
        $this->documentStates[$oid] = self::STATE_MANAGED;
1064
1065
        if ($upsert) {
1066 491
            $this->scheduleForUpsert($class, $document);
1067
        } else {
1068 491
            $this->scheduleForInsert($class, $document);
1069
        }
1070 491
    }
1071 491
1072 491
    /**
1073
     * Executes all document insertions for documents of the specified type.
1074
     *
1075 491
     * @param array $documents Array of documents to insert
1076
     * @param array $options   Array of options to be used with batchInsert()
1077 490
     */
1078 490
    private function executeInserts(ClassMetadata $class, array $documents, array $options = [])
1079
    {
1080 490
        $persister = $this->getDocumentPersister($class->name);
1081
1082
        foreach ($documents as $oid => $document) {
1083
            $persister->addInsert($document);
1084
            unset($this->documentInsertions[$oid]);
1085
        }
1086
1087
        $persister->executeInserts($options);
1088 84
1089
        foreach ($documents as $document) {
1090 84
            $this->lifecycleEventManager->postPersist($class, $document);
1091
        }
1092 84
    }
1093 84
1094 84
    /**
1095
     * Executes all document upserts for documents of the specified type.
1096
     *
1097 84
     * @param array $documents Array of documents to upsert
1098
     * @param array $options   Array of options to be used with batchInsert()
1099 84
     */
1100 84
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = [])
1101
    {
1102 84
        $persister = $this->getDocumentPersister($class->name);
1103
1104
        foreach ($documents as $oid => $document) {
1105
            $persister->addUpsert($document);
1106
            unset($this->documentUpserts[$oid]);
1107
        }
1108
1109
        $persister->executeUpserts($options);
1110 205
1111
        foreach ($documents as $document) {
1112 205
            $this->lifecycleEventManager->postPersist($class, $document);
1113
        }
1114
    }
1115
1116 205
    /**
1117 205
     * Executes all document updates for documents of the specified type.
1118
     *
1119 205
     * @param array $documents Array of documents to update
1120 205
     * @param array $options   Array of options to be used with update()
1121
     */
1122 205
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = [])
1123 204
    {
1124
        if ($class->isReadOnly) {
1125
            return;
1126 201
        }
1127
1128 201
        $className = $class->name;
1129
        $persister = $this->getDocumentPersister($className);
1130 201
1131
        foreach ($documents as $oid => $document) {
1132
            $this->lifecycleEventManager->preUpdate($class, $document);
1133
1134
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1135
                $persister->update($document, $options);
1136
            }
1137
1138 66
            unset($this->documentUpdates[$oid]);
1139
1140 66
            $this->lifecycleEventManager->postUpdate($class, $document);
1141
        }
1142 66
    }
1143 66
1144 34
    /**
1145
     * Executes all document deletions for documents of the specified type.
1146
     *
1147 64
     * @param array $documents Array of documents to delete
1148 64
     * @param array $options   Array of options to be used with remove()
1149 64
     */
1150
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = [])
1151
    {
1152
        $persister = $this->getDocumentPersister($class->name);
1153
1154 64
        foreach ($documents as $oid => $document) {
1155 40
            if (! $class->isEmbeddedDocument) {
1156 30
                $persister->delete($document, $options);
1157
            }
1158
            unset(
1159 26
                $this->documentDeletions[$oid],
1160 26
                $this->documentIdentifiers[$oid],
1161 7
                $this->originalDocumentData[$oid]
1162
            );
1163
1164 22
            // Clear snapshot information for any referenced PersistentCollection
1165
            // http://www.doctrine-project.org/jira/browse/MODM-95
1166
            foreach ($class->associationMappings as $fieldMapping) {
1167
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1168
                    continue;
1169 64
                }
1170
1171 64
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1172
                if (! ($value instanceof PersistentCollectionInterface)) {
1173 64
                    continue;
1174
                }
1175
1176
                $value->clearSnapshot();
1177
            }
1178
1179
            // Document with this $oid after deletion treated as NEW, even if the $oid
1180
            // is obtained by a new document because the old one went out of scope.
1181
            $this->documentStates[$oid] = self::STATE_NEW;
1182
1183 531
            $this->lifecycleEventManager->postRemove($class, $document);
1184
        }
1185 531
    }
1186
1187 531
    /**
1188
     * Schedules a document for insertion into the database.
1189
     * If the document already has an identifier, it will be added to the
1190 531
     * identity map.
1191
     *
1192
     * @param object $document The document to schedule for insertion.
1193 531
     * @throws \InvalidArgumentException
1194
     */
1195
    public function scheduleForInsert(ClassMetadata $class, $document)
1196
    {
1197 531
        $oid = spl_object_hash($document);
1198
1199 531
        if (isset($this->documentUpdates[$oid])) {
1200 3
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1201
        }
1202
        if (isset($this->documentDeletions[$oid])) {
1203 528
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1204 528
        }
1205
        if (isset($this->documentInsertions[$oid])) {
1206
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1207
        }
1208
1209
        $this->documentInsertions[$oid] = $document;
1210
1211
        if (! isset($this->documentIdentifiers[$oid])) {
1212
            return;
1213 90
        }
1214
1215 90
        $this->addToIdentityMap($document);
1216
    }
1217 90
1218
    /**
1219
     * Schedules a document for upsert into the database and adds it to the
1220 90
     * identity map
1221
     *
1222
     * @param object $document The document to schedule for upsert.
1223 90
     * @throws \InvalidArgumentException
1224
     */
1225
    public function scheduleForUpsert(ClassMetadata $class, $document)
1226 90
    {
1227
        $oid = spl_object_hash($document);
1228
1229
        if ($class->isEmbeddedDocument) {
1230 90
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1231 90
        }
1232 90
        if (isset($this->documentUpdates[$oid])) {
1233 90
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1234
        }
1235
        if (isset($this->documentDeletions[$oid])) {
1236
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1237
        }
1238
        if (isset($this->documentUpserts[$oid])) {
1239
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1240
        }
1241 89
1242
        $this->documentUpserts[$oid] = $document;
1243 89
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1244
        $this->addToIdentityMap($document);
1245
    }
1246
1247
    /**
1248
     * Checks whether a document is scheduled for insertion.
1249
     *
1250
     * @param object $document
1251
     * @return bool
1252 5
     */
1253
    public function isScheduledForInsert($document)
1254 5
    {
1255
        return isset($this->documentInsertions[spl_object_hash($document)]);
1256
    }
1257
1258
    /**
1259
     * Checks whether a document is scheduled for upsert.
1260
     *
1261
     * @param object $document
1262
     * @return bool
1263 211
     */
1264
    public function isScheduledForUpsert($document)
1265 211
    {
1266 211
        return isset($this->documentUpserts[spl_object_hash($document)]);
1267
    }
1268
1269
    /**
1270 211
     * Schedules a document for being updated.
1271
     *
1272
     * @param object $document The document to schedule for being updated.
1273
     * @throws \InvalidArgumentException
1274 211
     */
1275 211
    public function scheduleForUpdate($document)
1276 211
    {
1277 79
        $oid = spl_object_hash($document);
1278
        if (! isset($this->documentIdentifiers[$oid])) {
1279
            throw new \InvalidArgumentException('Document has no identity.');
1280 210
        }
1281 210
1282
        if (isset($this->documentDeletions[$oid])) {
1283
            throw new \InvalidArgumentException('Document is removed.');
1284
        }
1285
1286
        if (isset($this->documentUpdates[$oid])
1287
            || isset($this->documentInsertions[$oid])
1288
            || isset($this->documentUpserts[$oid])) {
1289
            return;
1290
        }
1291 15
1292
        $this->documentUpdates[$oid] = $document;
1293 15
    }
1294
1295
    /**
1296 1
     * Checks whether a document is registered as dirty in the unit of work.
1297
     * Note: Is not very useful currently as dirty documents are only registered
1298 1
     * at commit time.
1299 1
     *
1300
     * @param object $document
1301
     * @return bool
1302
     */
1303
    public function isScheduledForUpdate($document)
1304
    {
1305
        return isset($this->documentUpdates[spl_object_hash($document)]);
1306
    }
1307
1308 71
    public function isScheduledForDirtyCheck($document)
1309
    {
1310 71
        $class = $this->dm->getClassMetadata(get_class($document));
1311
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1312 71
    }
1313 1
1314 1
    /**
1315
     * INTERNAL:
1316 1
     * Schedules a document for deletion.
1317 1
     *
1318
     * @param object $document
1319
     */
1320 70
    public function scheduleForDelete($document)
1321 2
    {
1322
        $oid = spl_object_hash($document);
1323
1324 69
        if (isset($this->documentInsertions[$oid])) {
1325 69
            if ($this->isInIdentityMap($document)) {
1326
                $this->removeFromIdentityMap($document);
1327 69
            }
1328
            unset($this->documentInsertions[$oid]);
1329
            return; // document has not been persisted yet, so nothing more to do.
1330 69
        }
1331
1332
        if (! $this->isInIdentityMap($document)) {
1333
            return; // ignore
1334 69
        }
1335 69
1336
        $this->removeFromIdentityMap($document);
1337
        $this->documentStates[$oid] = self::STATE_REMOVED;
1338
1339
        if (isset($this->documentUpdates[$oid])) {
1340
            unset($this->documentUpdates[$oid]);
1341
        }
1342
        if (isset($this->documentDeletions[$oid])) {
1343
            return;
1344 5
        }
1345
1346 5
        $this->documentDeletions[$oid] = $document;
1347
    }
1348
1349
    /**
1350
     * Checks whether a document is registered as removed/deleted with the unit
1351
     * of work.
1352
     *
1353
     * @param object $document
1354
     * @return bool
1355 229
     */
1356
    public function isScheduledForDelete($document)
1357 229
    {
1358 229
        return isset($this->documentDeletions[spl_object_hash($document)]);
1359 113
    }
1360 104
1361 229
    /**
1362
     * Checks whether a document is scheduled for insertion, update or deletion.
1363
     *
1364
     * @param object $document
1365
     * @return bool
1366
     */
1367
    public function isDocumentScheduled($document)
1368
    {
1369
        $oid = spl_object_hash($document);
1370
        return isset($this->documentInsertions[$oid]) ||
1371
            isset($this->documentUpserts[$oid]) ||
1372
            isset($this->documentUpdates[$oid]) ||
1373
            isset($this->documentDeletions[$oid]);
1374
    }
1375
1376
    /**
1377 629
     * INTERNAL:
1378
     * Registers a document in the identity map.
1379 629
     *
1380 629
     * Note that documents in a hierarchy are registered with the class name of
1381
     * the root document. Identifiers are serialized before being used as array
1382 629
     * keys to allow differentiation of equal, but not identical, values.
1383 42
     *
1384
     * @ignore
1385
     * @param object $document The document to register.
1386 629
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1387
     *                  the document in question is already managed.
1388 629
     */
1389 629
    public function addToIdentityMap($document)
1390 3
    {
1391
        $class = $this->dm->getClassMetadata(get_class($document));
1392
        $id = $this->getIdForIdentityMap($document);
1393 629
1394
        if (isset($this->identityMap[$class->name][$id])) {
1395
            return false;
1396
        }
1397
1398
        $this->identityMap[$class->name][$id] = $document;
1399
1400
        if ($document instanceof NotifyPropertyChanged &&
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\NotifyPropertyChanged does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
1401
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1402
            $document->addPropertyChangedListener($this);
1403
        }
1404
1405
        return true;
1406 601
    }
1407
1408 601
    /**
1409
     * Gets the state of a document with regard to the current unit of work.
1410 601
     *
1411 370
     * @param object   $document
1412
     * @param int|null $assume   The state to assume if the state is not yet known (not MANAGED or REMOVED).
1413
     *                           This parameter can be set to improve performance of document state detection
1414 601
     *                           by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1415
     *                           is either known or does not matter for the caller of the method.
1416 601
     * @return int The document state.
1417 164
     */
1418
    public function getDocumentState($document, $assume = null)
1419
    {
1420 598
        $oid = spl_object_hash($document);
1421 596
1422
        if (isset($this->documentStates[$oid])) {
1423
            return $this->documentStates[$oid];
1424
        }
1425
1426
        $class = $this->dm->getClassMetadata(get_class($document));
1427
1428
        if ($class->isEmbeddedDocument) {
1429
            return self::STATE_NEW;
1430
        }
1431 3
1432
        if ($assume !== null) {
1433 3
            return $assume;
1434 2
        }
1435
1436
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1437
         * known. Note that you cannot remember the NEW or DETACHED state in
1438 2
         * _documentStates since the UoW does not hold references to such
1439
         * objects and the object hash can be reused. More generally, because
1440
         * the state may "change" between NEW/DETACHED without the UoW being
1441
         * aware of it.
1442
         */
1443
        $id = $class->getIdentifierObject($document);
1444
1445 2
        if ($id === null) {
1446 1
            return self::STATE_NEW;
1447
        }
1448
1449
        // Check for a version field, if available, to avoid a DB lookup.
1450 2
        if ($class->isVersioned) {
1451 1
            return $class->getFieldValue($document, $class->versionField)
1452
                ? self::STATE_DETACHED
1453
                : self::STATE_NEW;
1454 1
        }
1455
1456
        // Last try before DB lookup: check the identity map.
1457
        if ($this->tryGetById($id, $class)) {
1458
            return self::STATE_DETACHED;
1459
        }
1460
1461
        // DB lookup
1462
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1463
            return self::STATE_DETACHED;
1464
        }
1465
1466
        return self::STATE_NEW;
1467 83
    }
1468
1469 83
    /**
1470
     * INTERNAL:
1471
     * Removes a document from the identity map. This effectively detaches the
1472 83
     * document from the persistence management of Doctrine.
1473
     *
1474
     * @ignore
1475
     * @param object $document
1476 83
     * @throws \InvalidArgumentException
1477 83
     * @return bool
1478
     */
1479 83
    public function removeFromIdentityMap($document)
1480 83
    {
1481 83
        $oid = spl_object_hash($document);
1482 83
1483
        // Check if id is registered first
1484
        if (! isset($this->documentIdentifiers[$oid])) {
1485
            return false;
1486
        }
1487
1488
        $class = $this->dm->getClassMetadata(get_class($document));
1489
        $id = $this->getIdForIdentityMap($document);
1490
1491
        if (isset($this->identityMap[$class->name][$id])) {
1492
            unset($this->identityMap[$class->name][$id]);
1493
            $this->documentStates[$oid] = self::STATE_DETACHED;
1494
            return true;
1495
        }
1496
1497
        return false;
1498 39
    }
1499
1500 39
    /**
1501
     * INTERNAL:
1502
     * Gets a document in the identity map by its identifier hash.
1503
     *
1504 39
     * @ignore
1505
     * @param mixed         $id    Document identifier
1506 39
     * @param ClassMetadata $class Document class
1507
     * @return object
1508
     * @throws InvalidArgumentException If the class does not have an identifier.
1509
     */
1510
    public function getById($id, ClassMetadata $class)
1511
    {
1512
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1513
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1514
        }
1515
1516
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1517
1518
        return $this->identityMap[$class->name][$serializedId];
1519
    }
1520 296
1521
    /**
1522 296
     * INTERNAL:
1523
     * Tries to get a document by its identifier hash. If no document is found
1524
     * for the given hash, FALSE is returned.
1525
     *
1526 296
     * @ignore
1527
     * @param mixed         $id    Document identifier
1528 296
     * @param ClassMetadata $class Document class
1529
     * @return mixed The found document or FALSE.
1530
     * @throws InvalidArgumentException If the class does not have an identifier.
1531
     */
1532
    public function tryGetById($id, ClassMetadata $class)
1533
    {
1534
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1535
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1536
        }
1537 3
1538
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1539 3
1540 3
        return $this->identityMap[$class->name][$serializedId] ?? false;
1541 3
    }
1542
1543
    /**
1544
     * Schedules a document for dirty-checking at commit-time.
1545
     *
1546
     * @param object $document The document to schedule for dirty-checking.
1547
     * @todo Rename: scheduleForSynchronization
1548
     */
1549 79
    public function scheduleForDirtyCheck($document)
1550
    {
1551 79
        $class = $this->dm->getClassMetadata(get_class($document));
1552
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1553 79
    }
1554 6
1555
    /**
1556
     * Checks whether a document is registered in the identity map.
1557 77
     *
1558 77
     * @param object $document
1559
     * @return bool
1560 77
     */
1561
    public function isInIdentityMap($document)
1562
    {
1563
        $oid = spl_object_hash($document);
1564
1565
        if (! isset($this->documentIdentifiers[$oid])) {
1566
            return false;
1567 629
        }
1568
1569 629
        $class = $this->dm->getClassMetadata(get_class($document));
1570
        $id = $this->getIdForIdentityMap($document);
1571 629
1572 134
        return isset($this->identityMap[$class->name][$id]);
1573
    }
1574 628
1575 628
    /**
1576
     * @param object $document
1577
     * @return string
1578 629
     */
1579
    private function getIdForIdentityMap($document)
1580
    {
1581
        $class = $this->dm->getClassMetadata(get_class($document));
1582
1583
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1584
            $id = spl_object_hash($document);
1585
        } else {
1586
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1587
            $id = serialize($class->getDatabaseIdentifierValue($id));
1588
        }
1589
1590
        return $id;
1591
    }
1592
1593
    /**
1594
     * INTERNAL:
1595
     * Checks whether an identifier exists in the identity map.
1596
     *
1597
     * @ignore
1598
     * @param string $id
1599
     * @param string $rootClassName
1600
     * @return bool
1601
     */
1602 599
    public function containsId($id, $rootClassName)
1603
    {
1604 599
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1605 599
    }
1606 1
1607
    /**
1608 598
     * Persists a document as part of the current unit of work.
1609 598
     *
1610 594
     * @param object $document The document to persist.
1611
     * @throws MongoDBException If trying to persist MappedSuperclass.
1612
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1613
     */
1614
    public function persist($document)
1615
    {
1616
        $class = $this->dm->getClassMetadata(get_class($document));
1617
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1618
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1619
        }
1620
        $visited = [];
1621
        $this->doPersist($document, $visited);
1622
    }
1623
1624
    /**
1625 598
     * Saves a document as part of the current unit of work.
1626
     * This method is internally called during save() cascades as it tracks
1627 598
     * the already visited documents to prevent infinite recursions.
1628 598
     *
1629 25
     * NOTE: This method always considers documents that are not yet known to
1630
     * this UnitOfWork as NEW.
1631
     *
1632 598
     * @param object $document The document to persist.
1633
     * @param array  $visited  The already visited documents.
1634 598
     * @throws \InvalidArgumentException
1635
     * @throws MongoDBException
1636 598
     */
1637
    private function doPersist($document, array &$visited)
1638 598
    {
1639
        $oid = spl_object_hash($document);
1640 51
        if (isset($visited[$oid])) {
1641
            return; // Prevent infinite recursion
1642
        }
1643 51
1644 598
        $visited[$oid] = $document; // Mark visited
1645 598
1646 596
        $class = $this->dm->getClassMetadata(get_class($document));
1647
1648 2
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1649
        switch ($documentState) {
1650 2
            case self::STATE_MANAGED:
1651
                // Nothing to do, except if policy is "deferred explicit"
1652 2
                if ($class->isChangeTrackingDeferredExplicit()) {
1653 2
                    $this->scheduleForDirtyCheck($document);
1654
                }
1655
                break;
1656
            case self::STATE_NEW:
1657
                if ($class->isFile) {
1658
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1659
                }
1660
1661
                $this->persistNew($class, $document);
1662
                break;
1663
1664 596
            case self::STATE_REMOVED:
1665 594
                // Document becomes managed again
1666
                unset($this->documentDeletions[$oid]);
1667
1668
                $this->documentStates[$oid] = self::STATE_MANAGED;
1669
                break;
1670
1671
            case self::STATE_DETACHED:
1672 70
                throw new \InvalidArgumentException(
1673
                    'Behavior of persist() for a detached document is not yet defined.'
1674 70
                );
1675 70
1676 70
            default:
1677
                throw MongoDBException::invalidDocumentState($documentState);
1678
        }
1679
1680
        $this->cascadePersist($document, $visited);
1681
    }
1682
1683
    /**
1684
     * Deletes a document as part of the current unit of work.
1685
     *
1686
     * @param object $document The document to remove.
1687
     */
1688 70
    public function remove($document)
1689
    {
1690 70
        $visited = [];
1691 70
        $this->doRemove($document, $visited);
1692 1
    }
1693
1694
    /**
1695 70
     * Deletes a document as part of the current unit of work.
1696
     *
1697
     * This method is internally called during delete() cascades as it tracks
1698
     * the already visited documents to prevent infinite recursions.
1699
     *
1700
     * @param object $document The document to delete.
1701 70
     * @param array  $visited  The map of the already visited documents.
1702
     * @throws MongoDBException
1703 70
     */
1704 70
    private function doRemove($document, array &$visited)
1705
    {
1706 70
        $oid = spl_object_hash($document);
1707 70
        if (isset($visited[$oid])) {
1708
            return; // Prevent infinite recursion
1709
        }
1710 70
1711 70
        $visited[$oid] = $document; // mark visited
1712 70
1713 70
        /* Cascade first, because scheduleForDelete() removes the entity from
1714
         * the identity map, which can cause problems when a lazy Proxy has to
1715
         * be initialized for the cascade operation.
1716
         */
1717
        $this->cascadeRemove($document, $visited);
1718
1719 70
        $class = $this->dm->getClassMetadata(get_class($document));
1720
        $documentState = $this->getDocumentState($document);
1721
        switch ($documentState) {
1722
            case self::STATE_NEW:
1723
            case self::STATE_REMOVED:
1724
                // nothing to do
1725
                break;
1726
            case self::STATE_MANAGED:
1727 12
                $this->lifecycleEventManager->preRemove($class, $document);
1728
                $this->scheduleForDelete($document);
1729 12
                break;
1730
            case self::STATE_DETACHED:
1731 12
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1732
            default:
1733
                throw MongoDBException::invalidDocumentState($documentState);
1734
        }
1735
    }
1736
1737
    /**
1738
     * Merges the state of the given detached document into this UnitOfWork.
1739
     *
1740
     * @param object $document
1741
     * @return object The managed copy of the document.
1742
     */
1743
    public function merge($document)
1744
    {
1745
        $visited = [];
1746
1747
        return $this->doMerge($document, $visited);
1748
    }
1749 12
1750
    /**
1751 12
     * Executes a merge operation on a document.
1752
     *
1753 12
     * @param object      $document
1754 1
     * @param array       $visited
1755
     * @param object|null $prevManagedCopy
1756
     * @param array|null  $assoc
1757 12
     *
1758
     * @return object The managed copy of the document.
1759 12
     *
1760
     * @throws InvalidArgumentException If the entity instance is NEW.
1761
     * @throws LockException If the document uses optimistic locking through a
1762
     *                       version attribute and the version check against the
1763
     *                       managed copy fails.
1764
     */
1765
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1766 12
    {
1767
        $oid = spl_object_hash($document);
1768 12
1769 12
        if (isset($visited[$oid])) {
1770
            return $visited[$oid]; // Prevent infinite recursion
1771
        }
1772
1773 12
        $visited[$oid] = $document; // mark visited
1774
1775 12
        $class = $this->dm->getClassMetadata(get_class($document));
1776 12
1777
        /* First we assume DETACHED, although it can still be NEW but we can
1778
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1779 12
         * an identity, we need to fetch it from the DB anyway in order to
1780 12
         * merge. MANAGED documents are ignored by the merge operation.
1781
         */
1782
        $managedCopy = $document;
1783 12
1784
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1785
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1786
                $document->__load();
1787 12
            }
1788
1789
            $identifier = $class->getIdentifier();
1790
            // We always have one element in the identifier array but it might be null
1791
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1792 12
            $managedCopy = null;
1793
1794 4
            // Try to fetch document from the database
1795 4
            if (! $class->isEmbeddedDocument && $id !== null) {
1796 3
                $managedCopy = $this->dm->find($class->name, $id);
1797
1798 4
                // Managed copy may be removed in which case we can't merge
1799
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1800
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1801 12
                }
1802
1803
                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...
1804
                    $managedCopy->__load();
1805
                }
1806
            }
1807
1808
            if ($managedCopy === null) {
1809
                // Create a new managed instance
1810
                $managedCopy = $class->newInstance();
1811
                if ($id !== null) {
1812 12
                    $class->setIdentifierValue($managedCopy, $id);
1813 12
                }
1814 12
                $this->persistNew($class, $managedCopy);
1815 12
            }
1816 12
1817 12
            if ($class->isVersioned) {
1818
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1819
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1820 12
1821
                // Throw exception if versions don't match
1822 12
                if ($managedCopyVersion !== $documentVersion) {
1823 6
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1824
                }
1825 6
            }
1826 2
1827 5
            // Merge state of $document into existing (managed) document
1828
            foreach ($class->reflClass->getProperties() as $prop) {
1829 1
                $name = $prop->name;
1830 4
                $prop->setAccessible(true);
1831
                if (! isset($class->associationMappings[$name])) {
1832
                    if (! $class->isIdentifier($name)) {
1833
                        $prop->setValue($managedCopy, $prop->getValue($document));
1834
                    }
1835
                } else {
1836
                    $assoc2 = $class->associationMappings[$name];
1837
1838
                    if ($assoc2['type'] === 'one') {
1839
                        $other = $prop->getValue($document);
1840
1841
                        if ($other === null) {
1842
                            $prop->setValue($managedCopy, null);
1843
                        } 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...
1844
                            // Do not merge fields marked lazy that have not been fetched
1845
                            continue;
1846
                        } elseif (! $assoc2['isCascadeMerge']) {
1847
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1848 5
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1849
                                /** @var ClassMetadata $targetClass */
1850
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1851 10
                                $relatedId = $targetClass->getIdentifierObject($other);
1852
1853 10
                                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...
1854
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1855
                                } else {
1856
                                    $other = $this
1857
                                        ->dm
1858 3
                                        ->getProxyFactory()
1859
                                        ->getProxy($assoc2['targetDocument'], [$targetClass->identifier => $relatedId]);
1860
                                    $this->registerManaged($other, $relatedId, []);
1861 10
                                }
1862
                            }
1863 10
1864 1
                            $prop->setValue($managedCopy, $other);
1865 1
                        }
1866 1
                    } else {
1867 1
                        $mergeCol = $prop->getValue($document);
1868
1869
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1870
                            /* Do not merge fields marked lazy that have not
1871
                             * been fetched. Keep the lazy persistent collection
1872
                             * of the managed copy.
1873
                             */
1874 10
                            continue;
1875 10
                        }
1876
1877
                        $managedCol = $prop->getValue($managedCopy);
1878 10
1879 3
                        if (! $managedCol) {
1880 3
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1881
                            $managedCol->setOwner($managedCopy, $assoc2);
1882 3
                            $prop->setValue($managedCopy, $managedCol);
1883
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1884
                        }
1885
1886
                        /* Note: do not process association's target documents.
1887
                         * They will be handled during the cascade. Initialize
1888
                         * and, if necessary, clear $managedCol for now.
1889
                         */
1890 12
                        if ($assoc2['isCascadeMerge']) {
1891 12
                            $managedCol->initialize();
1892
1893
                            // If $managedCol differs from the merged collection, clear and set dirty
1894
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1895
                                $managedCol->unwrap()->clear();
1896
                                $managedCol->setDirty(true);
1897
1898 12
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1899
                                    $this->scheduleForDirtyCheck($managedCopy);
1900
                                }
1901
                            }
1902
                        }
1903 12
                    }
1904 5
                }
1905 5
1906
                if (! $class->isChangeTrackingNotify()) {
1907 5
                    continue;
1908 3
                }
1909
1910 4
                // Just treat all properties as changed, there is no other choice.
1911
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1912 4
            }
1913 1
1914
            if ($class->isChangeTrackingDeferredExplicit()) {
1915
                $this->scheduleForDirtyCheck($document);
1916
            }
1917
        }
1918
1919 12
        if ($prevManagedCopy !== null) {
1920
            $assocField = $assoc['fieldName'];
1921 12
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1922
1923 12
            if ($assoc['type'] === 'one') {
1924
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1925
            } else {
1926
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1927
1928
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1929
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1930
                }
1931
            }
1932 11
        }
1933
1934 11
        // Mark the managed copy visited as well
1935 11
        $visited[spl_object_hash($managedCopy)] = true;
1936 11
1937
        $this->cascadeMerge($document, $managedCopy, $visited);
1938
1939
        return $managedCopy;
1940
    }
1941
1942
    /**
1943
     * Detaches a document from the persistence management. It's persistence will
1944
     * no longer be managed by Doctrine.
1945 16
     *
1946
     * @param object $document The document to detach.
1947 16
     */
1948 16
    public function detach($document)
1949 3
    {
1950
        $visited = [];
1951
        $this->doDetach($document, $visited);
1952 16
    }
1953
1954 16
    /**
1955 16
     * Executes a detach operation on the given document.
1956 16
     *
1957
     * @param object $document
1958 16
     * @param array  $visited
1959 16
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1960 16
     */
1961 16
    private function doDetach($document, array &$visited)
1962 16
    {
1963 16
        $oid = spl_object_hash($document);
1964 16
        if (isset($visited[$oid])) {
1965 16
            return; // Prevent infinite recursion
1966 16
        }
1967 16
1968
        $visited[$oid] = $document; // mark visited
1969 16
1970 3
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1971 3
            case self::STATE_MANAGED:
1972 3
                $this->removeFromIdentityMap($document);
1973
                unset(
1974
                    $this->documentInsertions[$oid],
1975 16
                    $this->documentUpdates[$oid],
1976 16
                    $this->documentDeletions[$oid],
1977
                    $this->documentIdentifiers[$oid],
1978
                    $this->documentStates[$oid],
1979
                    $this->originalDocumentData[$oid],
1980
                    $this->parentAssociations[$oid],
1981
                    $this->documentUpserts[$oid],
1982
                    $this->hasScheduledCollections[$oid],
1983
                    $this->embeddedDocumentsRegistry[$oid]
1984
                );
1985 21
                break;
1986
            case self::STATE_NEW:
1987 21
            case self::STATE_DETACHED:
1988 21
                return;
1989 20
        }
1990
1991
        $this->cascadeDetach($document, $visited);
1992
    }
1993
1994
    /**
1995
     * Refreshes the state of the given document from the database, overwriting
1996
     * any local, unpersisted changes.
1997
     *
1998 21
     * @param object $document The document to refresh.
1999
     * @throws \InvalidArgumentException If the document is not MANAGED.
2000 21
     */
2001 21
    public function refresh($document)
2002
    {
2003
        $visited = [];
2004
        $this->doRefresh($document, $visited);
2005 21
    }
2006
2007 21
    /**
2008
     * Executes a refresh operation on a document.
2009 21
     *
2010 21
     * @param object $document The document to refresh.
2011 1
     * @param array  $visited  The already visited documents during cascades.
2012
     * @throws \InvalidArgumentException If the document is not MANAGED.
2013
     */
2014 20
    private function doRefresh($document, array &$visited)
2015
    {
2016
        $oid = spl_object_hash($document);
2017 20
        if (isset($visited[$oid])) {
2018 20
            return; // Prevent infinite recursion
2019
        }
2020
2021
        $visited[$oid] = $document; // mark visited
2022
2023
        $class = $this->dm->getClassMetadata(get_class($document));
2024
2025
        if (! $class->isEmbeddedDocument) {
2026 20
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2027
                throw new \InvalidArgumentException('Document is not MANAGED.');
2028 20
            }
2029
2030 20
            $this->getDocumentPersister($class->name)->refresh($document);
2031 20
        }
2032
2033 17
        $this->cascadeRefresh($document, $visited);
2034 20
    }
2035
2036
    /**
2037 20
     * Cascades a refresh operation to associated documents.
2038 15
     *
2039 15
     * @param object $document
2040 15
     * @param array  $visited
2041
     */
2042 15
    private function cascadeRefresh($document, array &$visited)
2043
    {
2044 15
        $class = $this->dm->getClassMetadata(get_class($document));
2045 15
2046
        $associationMappings = array_filter(
2047 10
            $class->associationMappings,
2048 15
            function ($assoc) {
2049
                return $assoc['isCascadeRefresh'];
2050
            }
2051 20
        );
2052
2053
        foreach ($associationMappings as $mapping) {
2054
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2055
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2056
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2057
                    // Unwrap so that foreach() does not initialize
2058
                    $relatedDocuments = $relatedDocuments->unwrap();
2059 16
                }
2060
                foreach ($relatedDocuments as $relatedDocument) {
2061 16
                    $this->doRefresh($relatedDocument, $visited);
2062 16
                }
2063 16
            } elseif ($relatedDocuments !== null) {
2064 16
                $this->doRefresh($relatedDocuments, $visited);
2065
            }
2066 10
        }
2067 10
    }
2068 10
2069
    /**
2070 7
     * Cascades a detach operation to associated documents.
2071
     *
2072 10
     * @param object $document
2073 10
     * @param array  $visited
2074
     */
2075 10
    private function cascadeDetach($document, array &$visited)
2076 10
    {
2077
        $class = $this->dm->getClassMetadata(get_class($document));
2078
        foreach ($class->fieldMappings as $mapping) {
2079 16
            if (! $mapping['isCascadeDetach']) {
2080
                continue;
2081
            }
2082
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2083
            if (($relatedDocuments instanceof Collection || is_array($relatedDocuments))) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2084
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2085
                    // Unwrap so that foreach() does not initialize
2086
                    $relatedDocuments = $relatedDocuments->unwrap();
2087 12
                }
2088
                foreach ($relatedDocuments as $relatedDocument) {
2089 12
                    $this->doDetach($relatedDocument, $visited);
2090
                }
2091 12
            } elseif ($relatedDocuments !== null) {
2092 12
                $this->doDetach($relatedDocuments, $visited);
2093
            }
2094 12
        }
2095 12
    }
2096
    /**
2097
     * Cascades a merge operation to associated documents.
2098 12
     *
2099 11
     * @param object $document
2100
     * @param object $managedCopy
2101 11
     * @param array  $visited
2102 8
     */
2103
    private function cascadeMerge($document, $managedCopy, array &$visited)
2104 1
    {
2105
        $class = $this->dm->getClassMetadata(get_class($document));
2106
2107 8
        $associationMappings = array_filter(
2108 8
            $class->associationMappings,
2109
            function ($assoc) {
2110 6
                return $assoc['isCascadeMerge'];
2111 11
            }
2112
        );
2113
2114 12
        foreach ($associationMappings as $assoc) {
2115
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2116
2117
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2118
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2119
                    // Collections are the same, so there is nothing to do
2120
                    continue;
2121
                }
2122 596
2123
                foreach ($relatedDocuments as $relatedDocument) {
2124 596
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2125
                }
2126 596
            } elseif ($relatedDocuments !== null) {
2127 596
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2128
            }
2129 454
        }
2130 596
    }
2131
2132
    /**
2133 596
     * Cascades the save operation to associated documents.
2134 410
     *
2135
     * @param object $document
2136 410
     * @param array  $visited
2137 339
     */
2138 12
    private function cascadePersist($document, array &$visited)
2139 2
    {
2140
        $class = $this->dm->getClassMetadata(get_class($document));
2141
2142 12
        $associationMappings = array_filter(
2143
            $class->associationMappings,
2144
            function ($assoc) {
2145 339
                return $assoc['isCascadePersist'];
2146 339
            }
2147 175
        );
2148 104
2149 104
        foreach ($associationMappings as $fieldName => $mapping) {
2150 1
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2151 1
2152
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2153 104
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2154 104
                    if ($relatedDocuments->getOwner() !== $document) {
2155
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2156 339
                    }
2157
                    // Unwrap so that foreach() does not initialize
2158 325
                    $relatedDocuments = $relatedDocuments->unwrap();
2159 128
                }
2160 67
2161 67
                $count = 0;
2162 3
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2163 3
                    if (! empty($mapping['embedded'])) {
2164
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2165 67
                        if ($knownParent && $knownParent !== $document) {
2166
                            $relatedDocument = clone $relatedDocument;
2167 410
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2168
                        }
2169
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2170 594
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2171
                    }
2172
                    $this->doPersist($relatedDocument, $visited);
2173
                }
2174
            } elseif ($relatedDocuments !== null) {
2175
                if (! empty($mapping['embedded'])) {
2176
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2177
                    if ($knownParent && $knownParent !== $document) {
2178 70
                        $relatedDocuments = clone $relatedDocuments;
2179
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2180 70
                    }
2181 70
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2182 70
                }
2183 69
                $this->doPersist($relatedDocuments, $visited);
2184
            }
2185 35
        }
2186 2
    }
2187
2188
    /**
2189 35
     * Cascades the delete operation to associated documents.
2190 35
     *
2191
     * @param object $document
2192 24
     * @param array  $visited
2193 24
     */
2194
    private function cascadeRemove($document, array &$visited)
2195 24
    {
2196 35
        $class = $this->dm->getClassMetadata(get_class($document));
2197
        foreach ($class->fieldMappings as $mapping) {
2198
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2199 70
                continue;
2200
            }
2201
            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...
2202
                $document->__load();
2203
            }
2204
2205
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2206
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2207
                // If its a PersistentCollection initialization is intended! No unwrap!
2208
                foreach ($relatedDocuments as $relatedDocument) {
2209
                    $this->doRemove($relatedDocument, $visited);
2210 8
                }
2211
            } elseif ($relatedDocuments !== null) {
2212 8
                $this->doRemove($relatedDocuments, $visited);
2213 1
            }
2214
        }
2215
    }
2216 7
2217 7
    /**
2218
     * Acquire a lock on the given document.
2219 7
     *
2220 2
     * @param object $document
2221 1
     * @param int    $lockMode
2222
     * @param int    $lockVersion
2223
     * @throws LockException
2224 1
     * @throws \InvalidArgumentException
2225 1
     */
2226 1
    public function lock($document, $lockMode, $lockVersion = null)
2227 1
    {
2228
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2229
            throw new \InvalidArgumentException('Document is not MANAGED.');
2230 5
        }
2231 5
2232
        $documentName = get_class($document);
2233 5
        $class = $this->dm->getClassMetadata($documentName);
2234
2235
        if ($lockMode === LockMode::OPTIMISTIC) {
2236
            if (! $class->isVersioned) {
2237
                throw LockException::notVersioned($documentName);
2238
            }
2239
2240
            if ($lockVersion !== null) {
2241 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2242
                if ($documentVersion !== $lockVersion) {
2243 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2244
                }
2245
            }
2246 1
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2247 1
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2248 1
        }
2249
    }
2250
2251
    /**
2252
     * Releases a lock on the given document.
2253
     *
2254
     * @param object $document
2255 372
     * @throws \InvalidArgumentException
2256
     */
2257 372
    public function unlock($document)
2258 364
    {
2259 364
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2260 364
            throw new \InvalidArgumentException('Document is not MANAGED.');
2261 364
        }
2262 364
        $documentName = get_class($document);
2263 364
        $this->getDocumentPersister($documentName)->unlock($document);
2264 364
    }
2265 364
2266 364
    /**
2267 364
     * Clears the UnitOfWork.
2268 364
     *
2269 364
     * @param string|null $documentName if given, only documents of this type will get detached.
2270 364
     */
2271 364
    public function clear($documentName = null)
2272 364
    {
2273 364
        if ($documentName === null) {
2274
            $this->identityMap =
2275 8
            $this->documentIdentifiers =
2276 8
            $this->originalDocumentData =
2277 8
            $this->documentChangeSets =
2278 5
            $this->documentStates =
2279
            $this->scheduledForDirtyCheck =
2280
            $this->documentInsertions =
2281 5
            $this->documentUpserts =
2282 5
            $this->documentUpdates =
2283
            $this->documentDeletions =
2284
            $this->collectionUpdates =
2285
            $this->collectionDeletions =
2286
            $this->parentAssociations =
2287 372
            $this->embeddedDocumentsRegistry =
2288 372
            $this->orphanRemovals =
2289
            $this->hasScheduledCollections = [];
2290
        } else {
2291
            $visited = [];
2292
            foreach ($this->identityMap as $className => $documents) {
2293
                if ($className !== $documentName) {
2294
                    continue;
2295
                }
2296
2297
                foreach ($documents as $document) {
2298
                    $this->doDetach($document, $visited);
2299
                }
2300
            }
2301
        }
2302
2303 47
        if (! $this->evm->hasListeners(Events::onClear)) {
2304
            return;
2305 47
        }
2306 47
2307
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2308
    }
2309
2310
    /**
2311
     * INTERNAL:
2312
     * Schedules an embedded document for removal. The remove() operation will be
2313
     * invoked on that document at the beginning of the next commit of this
2314
     * UnitOfWork.
2315 100
     *
2316
     * @ignore
2317 100
     * @param object $document
2318 100
     */
2319 100
    public function scheduleOrphanRemoval($document)
2320
    {
2321
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2322
    }
2323
2324
    /**
2325
     * INTERNAL:
2326
     * Unschedules an embedded or referenced object for removal.
2327
     *
2328
     * @ignore
2329
     * @param object $document
2330
     */
2331
    public function unscheduleOrphanRemoval($document)
2332 8
    {
2333
        $oid = spl_object_hash($document);
2334 8
        unset($this->orphanRemovals[$oid]);
2335 8
    }
2336 6
2337 2
    /**
2338 2
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2339 1
     *  1) sets owner if it was cloned
2340
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2341 2
     *  3) NOP if state is OK
2342 2
     * Returned collection should be used from now on (only important with 2nd point)
2343 2
     *
2344 2
     * @param object $document
2345
     * @param string $propName
2346
     * @return PersistentCollectionInterface
2347
     */
2348 2
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2349
    {
2350 6
        $owner = $coll->getOwner();
2351
        if ($owner === null) { // cloned
2352
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2353
        } elseif ($owner !== $document) { // no clone, we have to fix
2354
            if (! $coll->isInitialized()) {
2355
                $coll->initialize(); // we have to do this otherwise the cols share state
2356
            }
2357
            $newValue = clone $coll;
2358 35
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2359
            $class->reflFields[$propName]->setValue($document, $newValue);
2360 35
            if ($this->isScheduledForUpdate($document)) {
2361 35
                // @todo following line should be superfluous once collections are stored in change sets
2362 35
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2363
            }
2364
            return $newValue;
2365
        }
2366 35
        return $coll;
2367 35
    }
2368 35
2369
    /**
2370
     * INTERNAL:
2371
     * Schedules a complete collection for removal when this UnitOfWork commits.
2372
     *
2373
     */
2374
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2375 192
    {
2376
        $oid = spl_object_hash($coll);
2377 192
        unset($this->collectionUpdates[$oid]);
2378
        if (isset($this->collectionDeletions[$oid])) {
2379
            return;
2380
        }
2381
2382
        $this->collectionDeletions[$oid] = $coll;
2383
        $this->scheduleCollectionOwner($coll);
2384
    }
2385 205
2386
    /**
2387 205
     * Checks whether a PersistentCollection is scheduled for deletion.
2388 205
     *
2389 205
     * @return bool
2390
     */
2391
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2392 5
    {
2393 5
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2394 5
    }
2395 5
2396
    /**
2397
     * INTERNAL:
2398
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2399
     *
2400
     */
2401
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
2402 226
    {
2403
        $oid = spl_object_hash($coll);
2404 226
        if (! isset($this->collectionDeletions[$oid])) {
2405 226
            return;
2406
        }
2407
2408
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2409 23
        unset($this->collectionDeletions[$oid]);
2410
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2411 226
    }
2412 226
2413 7
    /**
2414
     * INTERNAL:
2415
     * Schedules a collection for update when this UnitOfWork commits.
2416 226
     *
2417 226
     */
2418 226
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2419
    {
2420
        $mapping = $coll->getMapping();
2421
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2422
            /* There is no need to $unset collection if it will be $set later
2423
             * This is NOP if collection is not scheduled for deletion
2424
             */
2425 205
            $this->unscheduleCollectionDeletion($coll);
2426
        }
2427 205
        $oid = spl_object_hash($coll);
2428 205
        if (isset($this->collectionUpdates[$oid])) {
2429 35
            return;
2430
        }
2431
2432 195
        $this->collectionUpdates[$oid] = $coll;
2433 195
        $this->scheduleCollectionOwner($coll);
2434 195
    }
2435 195
2436
    /**
2437
     * INTERNAL:
2438
     * Unschedules a collection from being updated when this UnitOfWork commits.
2439
     *
2440
     */
2441
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
2442 114
    {
2443
        $oid = spl_object_hash($coll);
2444 114
        if (! isset($this->collectionUpdates[$oid])) {
2445
            return;
2446
        }
2447
2448
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2449
        unset($this->collectionUpdates[$oid]);
2450
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2451
    }
2452
2453
    /**
2454
     * Checks whether a PersistentCollection is scheduled for update.
2455 550
     *
2456
     * @return bool
2457 550
     */
2458
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2459 550
    {
2460
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2461
    }
2462
2463
    /**
2464
     * INTERNAL:
2465
     * Gets PersistentCollections that have been visited during computing change
2466
     * set of $document
2467
     *
2468
     * @param object $document
2469 550
     * @return PersistentCollectionInterface[]
2470
     */
2471 550
    public function getVisitedCollections($document)
2472
    {
2473 550
        $oid = spl_object_hash($document);
2474
2475
        return $this->visitedCollections[$oid] ?? [];
2476
    }
2477
2478
    /**
2479
     * INTERNAL:
2480
     * Gets PersistentCollections that are scheduled to update and related to $document
2481
     *
2482
     * @param object $document
2483 44
     * @return array
2484
     */
2485 44
    public function getScheduledCollections($document)
2486
    {
2487
        $oid = spl_object_hash($document);
2488
2489
        return $this->hasScheduledCollections[$oid] ?? [];
2490
    }
2491
2492
    /**
2493
     * Checks whether the document is related to a PersistentCollection
2494
     * scheduled for update or deletion.
2495
     *
2496
     * @param object $document
2497
     * @return bool
2498
     */
2499
    public function hasScheduledCollections($document)
2500 228
    {
2501
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2502 228
    }
2503 228
2504
    /**
2505 228
     * Marks the PersistentCollection's top-level owner as having a relation to
2506 19
     * a collection scheduled for update or deletion.
2507 19
     *
2508 19
     * If the owner is not scheduled for any lifecycle action, it will be
2509 19
     * scheduled for update to ensure that versioning takes place if necessary.
2510
     *
2511 19
     * If the collection is nested within atomic collection, it is immediately
2512 3
     * unscheduled and atomic one is scheduled for update instead. This makes
2513 3
     * calculating update data way easier.
2514 3
     *
2515 3
     */
2516 3
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2517
    {
2518
        $document = $this->getOwningDocument($coll->getOwner());
2519
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2520 228
2521 223
        if ($document !== $coll->getOwner()) {
2522
            $parent = $coll->getOwner();
2523
            $mapping = [];
2524 39
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2525 39
                list($mapping, $parent, ) = $parentAssoc;
2526
            }
2527
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2528
                $class = $this->dm->getClassMetadata(get_class($document));
2529
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2530
                $this->scheduleCollectionUpdate($atomicCollection);
2531
                $this->unscheduleCollectionDeletion($coll);
2532
                $this->unscheduleCollectionUpdate($coll);
2533
            }
2534
        }
2535
2536
        if ($this->isDocumentScheduled($document)) {
2537
            return;
2538 230
        }
2539
2540 230
        $this->scheduleForUpdate($document);
2541 230
    }
2542 33
2543
    /**
2544 33
     * Get the top-most owning document of a given document
2545
     *
2546
     * If a top-level document is provided, that same document will be returned.
2547
     * For an embedded document, we will walk through parent associations until
2548 33
     * we find a top-level document.
2549 33
     *
2550
     * @param object $document
2551
     * @throws \UnexpectedValueException When a top-level document could not be found.
2552 230
     * @return object
2553
     */
2554
    public function getOwningDocument($document)
2555
    {
2556
        $class = $this->dm->getClassMetadata(get_class($document));
2557
        while ($class->isEmbeddedDocument) {
2558
            $parentAssociation = $this->getParentAssociation($document);
2559
2560
            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...
2561
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2562
            }
2563 218
2564
            list(, $document, ) = $parentAssociation;
2565 218
            $class = $this->dm->getClassMetadata(get_class($document));
2566
        }
2567 218
2568 218
        return $document;
2569 21
    }
2570 198
2571
    /**
2572
     * Gets the class name for an association (embed or reference) with respect
2573
     * to any discriminator value.
2574 218
     *
2575 21
     * @param array      $mapping Field mapping for the association
2576 21
     * @param array|null $data    Data for the embedded document or reference
2577
     * @return string Class name.
2578
     */
2579 198
    public function getClassNameForAssociation(array $mapping, $data)
2580
    {
2581 198
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2582 15
2583 184
        $discriminatorValue = null;
2584 1
        if (isset($discriminatorField, $data[$discriminatorField])) {
2585
            $discriminatorValue = $data[$discriminatorField];
2586
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2587 198
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2588 16
        }
2589
2590
        if ($discriminatorValue !== null) {
2591 183
            return $mapping['discriminatorMap'][$discriminatorValue]
2592
                ?? $discriminatorValue;
2593
        }
2594
2595
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2596
2597
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2598
            $discriminatorValue = $data[$class->discriminatorField];
2599
        } elseif ($class->defaultDiscriminatorValue !== null) {
2600
            $discriminatorValue = $class->defaultDiscriminatorValue;
2601
        }
2602
2603
        if ($discriminatorValue !== null) {
2604
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2605
        }
2606 381
2607
        return $mapping['targetDocument'];
2608 381
    }
2609
2610
    /**
2611 381
     * INTERNAL:
2612 381
     * Creates a document. Used for reconstitution of documents during hydration.
2613 17
     *
2614 373
     * @ignore
2615 2
     * @param string $className The name of the document class.
2616
     * @param array  $data      The data for the document.
2617
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the document.
2618 381
     * @param object $document  The document to be hydrated into in case of creation
2619 18
     * @return object The document instance.
2620
     * @internal Highly performance-sensitive method.
2621 18
     */
2622
    public function getOrCreateDocument($className, $data, &$hints = [], $document = null)
2623 18
    {
2624
        $class = $this->dm->getClassMetadata($className);
2625
2626 381
        // @TODO figure out how to remove this
2627 2
        $discriminatorValue = null;
2628 2
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2629 2
            $discriminatorValue = $data[$class->discriminatorField];
2630
        } elseif (isset($class->defaultDiscriminatorValue)) {
2631
            $discriminatorValue = $class->defaultDiscriminatorValue;
2632 380
        }
2633 380
2634 380
        if ($discriminatorValue !== null) {
2635 380
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2636 377
2637 377
            $class = $this->dm->getClassMetadata($className);
2638 377
2639
            unset($data[$class->discriminatorField]);
2640
        }
2641 380
2642 380
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2643 90
            $document = $class->newInstance();
2644 90
            $this->hydratorFactory->hydrate($document, $data, $hints);
2645 90
            return $document;
2646 14
        }
2647 14
2648 14
        $isManagedObject = false;
2649 14
        $serializedId = null;
2650
        $id = null;
2651
        if (! $class->isQueryResultDocument) {
2652 82
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2653
            $serializedId = serialize($id);
2654 90
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2655 38
        }
2656 90
2657
        $oid = null;
2658
        if ($isManagedObject) {
2659 342
            $document = $this->identityMap[$class->name][$serializedId];
2660 342
            $oid = spl_object_hash($document);
2661
            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...
2662
                $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...
2663 342
                $overrideLocalValues = true;
2664 338
                if ($document instanceof NotifyPropertyChanged) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\NotifyPropertyChanged does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
2665 338
                    $document->addPropertyChangedListener($this);
2666 338
                }
2667 338
            } else {
2668
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2669
            }
2670 342
            if ($overrideLocalValues) {
2671
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2672 342
                $this->originalDocumentData[$oid] = $data;
2673 338
            }
2674
        } else {
2675
            if ($document === null) {
2676
                $document = $class->newInstance();
2677 380
            }
2678
2679
            if (! $class->isQueryResultDocument) {
2680
                $this->registerManaged($document, $id, $data);
2681
                $oid = spl_object_hash($document);
2682
                $this->documentStates[$oid] = self::STATE_MANAGED;
2683
                $this->identityMap[$class->name][$serializedId] = $document;
2684
            }
2685 164
2686
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2687 164
2688 164
            if (! $class->isQueryResultDocument) {
2689 164
                $this->originalDocumentData[$oid] = $data;
2690
            }
2691
        }
2692
2693
        return $document;
2694
    }
2695
2696
    /**
2697
     * Initializes (loads) an uninitialized persistent collection of a document.
2698
     *
2699
     * @param PersistentCollectionInterface $collection The collection to initialize.
2700
     */
2701
    public function loadCollection(PersistentCollectionInterface $collection)
2702
    {
2703
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2704
        $this->lifecycleEventManager->postCollectionLoad($collection);
2705
    }
2706
2707
    /**
2708 1
     * Gets the identity map of the UnitOfWork.
2709
     *
2710 1
     * @return array
2711
     */
2712 1
    public function getIdentityMap()
2713
    {
2714
        return $this->identityMap;
2715 59
    }
2716
2717 59
    /**
2718 59
     * Gets the original data of a document. The original data is the data that was
2719 59
     * present at the time the document was reconstituted from the database.
2720 59
     *
2721
     * @param object $document
2722
     * @return array
2723
     */
2724
    public function getOriginalDocumentData($document)
2725
    {
2726
        $oid = spl_object_hash($document);
2727
2728
        return $this->originalDocumentData[$oid] ?? [];
2729
    }
2730
2731 3
    public function setOriginalDocumentData($document, array $data)
2732
    {
2733 3
        $oid = spl_object_hash($document);
2734 3
        $this->originalDocumentData[$oid] = $data;
2735
        unset($this->documentChangeSets[$oid]);
2736
    }
2737
2738
    /**
2739
     * INTERNAL:
2740
     * Sets a property value of the original data array of a document.
2741
     *
2742 414
     * @ignore
2743
     * @param string $oid
2744 414
     * @param string $property
2745
     * @param mixed  $value
2746
     */
2747
    public function setOriginalDocumentProperty($oid, $property, $value)
2748
    {
2749
        $this->originalDocumentData[$oid][$property] = $value;
2750
    }
2751
2752
    /**
2753
     * Gets the identifier of a document.
2754
     *
2755
     * @param object $document
2756
     * @return mixed The identifier value
2757
     */
2758
    public function getDocumentIdentifier($document)
2759
    {
2760
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2761
    }
2762
2763 2
    /**
2764
     * Checks whether the UnitOfWork has any pending insertions.
2765 2
     *
2766 2
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2767 2
     */
2768
    public function hasPendingInsertions()
2769 2
    {
2770
        return ! empty($this->documentInsertions);
2771
    }
2772
2773
    /**
2774
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2775
     * number of documents in the identity map.
2776
     *
2777
     * @return int
2778
     */
2779
    public function size()
2780
    {
2781
        $count = 0;
2782
        foreach ($this->identityMap as $documentSet) {
2783
            $count += count($documentSet);
2784
        }
2785
        return $count;
2786
    }
2787 363
2788
    /**
2789 363
     * INTERNAL:
2790 363
     * Registers a document as managed.
2791
     *
2792 363
     * TODO: This method assumes that $id is a valid PHP identifier for the
2793 92
     * document class. If the class expects its database identifier to be an
2794
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2795 357
     * document identifiers map will become inconsistent with the identity map.
2796
     * In the future, we may want to round-trip $id through a PHP and database
2797
     * conversion and throw an exception if it's inconsistent.
2798 363
     *
2799 363
     * @param object $document The document.
2800 363
     * @param array  $id       The identifier values.
2801 363
     * @param array  $data     The original document data.
2802
     */
2803
    public function registerManaged($document, $id, $data)
2804
    {
2805
        $oid = spl_object_hash($document);
2806
        $class = $this->dm->getClassMetadata(get_class($document));
2807
2808
        if (! $class->identifier || $id === null) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2809
            $this->documentIdentifiers[$oid] = $oid;
2810
        } else {
2811
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2812
        }
2813
2814
        $this->documentStates[$oid] = self::STATE_MANAGED;
2815
        $this->originalDocumentData[$oid] = $data;
2816
        $this->addToIdentityMap($document);
2817
    }
2818
2819
    /**
2820
     * INTERNAL:
2821
     * Clears the property changeset of the document with the given OID.
2822
     *
2823
     * @param string $oid The document's OID.
2824 2
     */
2825
    public function clearDocumentChangeSet($oid)
2826 2
    {
2827 2
        $this->documentChangeSets[$oid] = [];
2828
    }
2829 2
2830 1
    /* PropertyChangedListener implementation */
2831
2832
    /**
2833
     * Notifies this UnitOfWork of a property change in a document.
2834 2
     *
2835 2
     * @param object $document     The document that owns the property.
2836
     * @param string $propertyName The name of the property that changed.
2837
     * @param mixed  $oldValue     The old value of the property.
2838
     * @param mixed  $newValue     The new value of the property.
2839 2
     */
2840 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2841
    {
2842
        $oid = spl_object_hash($document);
2843
        $class = $this->dm->getClassMetadata(get_class($document));
2844
2845
        if (! isset($class->fieldMappings[$propertyName])) {
2846
            return; // ignore non-persistent fields
2847 2
        }
2848
2849 2
        // Update changeset and mark document for synchronization
2850
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2851
        if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2852
            return;
2853
        }
2854
2855
        $this->scheduleForDirtyCheck($document);
2856
    }
2857 1
2858
    /**
2859 1
     * Gets the currently scheduled document insertions in this UnitOfWork.
2860
     *
2861
     * @return array
2862
     */
2863
    public function getScheduledDocumentInsertions()
2864
    {
2865
        return $this->documentInsertions;
2866
    }
2867 1
2868
    /**
2869 1
     * Gets the currently scheduled document upserts in this UnitOfWork.
2870
     *
2871
     * @return array
2872
     */
2873
    public function getScheduledDocumentUpserts()
2874
    {
2875
        return $this->documentUpserts;
2876
    }
2877
2878
    /**
2879
     * Gets the currently scheduled document updates in this UnitOfWork.
2880
     *
2881
     * @return array
2882
     */
2883
    public function getScheduledDocumentUpdates()
2884
    {
2885
        return $this->documentUpdates;
2886
    }
2887
2888
    /**
2889
     * Gets the currently scheduled document deletions in this UnitOfWork.
2890
     *
2891
     * @return array
2892
     */
2893
    public function getScheduledDocumentDeletions()
2894
    {
2895
        return $this->documentDeletions;
2896
    }
2897
2898
    /**
2899
     * Get the currently scheduled complete collection deletions
2900
     *
2901
     * @return array
2902
     */
2903
    public function getScheduledCollectionDeletions()
2904
    {
2905
        return $this->collectionDeletions;
2906
    }
2907
2908
    /**
2909
     * Gets the currently scheduled collection inserts, updates and deletes.
2910
     *
2911
     * @return array
2912
     */
2913
    public function getScheduledCollectionUpdates()
2914
    {
2915
        return $this->collectionUpdates;
2916
    }
2917
2918
    /**
2919
     * Helper method to initialize a lazy loading proxy or persistent collection.
2920
     *
2921
     * @param object $obj
2922
     */
2923
    public function initializeObject($obj)
2924
    {
2925
        if ($obj instanceof Proxy) {
2926
            $obj->__load();
2927
        } elseif ($obj instanceof PersistentCollectionInterface) {
2928
            $obj->initialize();
2929
        }
2930
    }
2931
2932
    private function objToStr($obj)
2933
    {
2934
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2935
    }
2936
}
2937