UnitOfWork::doMerge()   F
last analyzed

Complexity

Conditions 42
Paths 617

Size

Total Lines 181

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 77
CRAP Score 46.3208

Importance

Changes 0
Metric Value
dl 0
loc 181
ccs 77
cts 89
cp 0.8652
rs 0.4255
c 0
b 0
f 0
cc 42
nc 617
nop 4
crap 46.3208

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
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\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
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 InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use function array_filter;
29
use function count;
30
use function get_class;
31
use function in_array;
32
use function is_array;
33
use function is_object;
34
use function method_exists;
35
use function preg_match;
36
use function serialize;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * The UnitOfWork is responsible for tracking changes to objects during an
42
 * "object-level" transaction and for writing out changes to the database
43
 * in the correct order.
44
 */
45
final class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    public const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    public const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    public const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    public const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = [];
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = [];
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     *
104
     * @var array
105
     */
106
    private $originalDocumentData = [];
107
108
    /**
109
     * Map of document changes. Keys are object ids (spl_object_hash).
110
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
111
     *
112
     * @var array
113
     */
114
    private $documentChangeSets = [];
115
116
    /**
117
     * The (cached) states of any known documents.
118
     * Keys are object ids (spl_object_hash).
119
     *
120
     * @var array
121
     */
122
    private $documentStates = [];
123
124
    /**
125
     * Map of documents that are scheduled for dirty checking at commit time.
126
     *
127
     * Documents are grouped by their class name, and then indexed by their SPL
128
     * object hash. This is only used for documents with a change tracking
129
     * policy of DEFERRED_EXPLICIT.
130
     *
131
     * @var array
132
     */
133
    private $scheduledForSynchronization = [];
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = [];
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = [];
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = [];
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = [];
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = [];
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = [];
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = [];
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = [];
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = [];
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = [];
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder|null
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents.
244
     *
245
     * @var array
246
     */
247
    private $parentAssociations = [];
248
249
    /** @var LifecycleEventManager */
250
    private $lifecycleEventManager;
251
252
    /**
253
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
254
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
255
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
256
     *
257
     * @var array
258
     */
259
    private $embeddedDocumentsRegistry = [];
260
261
    /** @var int */
262
    private $commitsInProgress = 0;
263
264
    /**
265
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
266
     */
267 1832
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269 1832
        $this->dm                    = $dm;
270 1832
        $this->evm                   = $evm;
271 1832
        $this->hydratorFactory       = $hydratorFactory;
272 1832
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1832
    }
274
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     *
279
     * @internal
280
     */
281 1244
    public function getPersistenceBuilder() : PersistenceBuilder
282
    {
283 1244
        if (! $this->persistenceBuilder) {
284 1244
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
285
        }
286
287 1244
        return $this->persistenceBuilder;
288
    }
289
290
    /**
291
     * Sets the parent association for a given embedded document.
292
     *
293
     * @internal
294
     */
295 209
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
296
    {
297 209
        $oid                                   = spl_object_hash($document);
298 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
299 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
300 209
    }
301
302
    /**
303
     * Gets the parent association for a given embedded document.
304
     *
305
     *     <code>
306
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
307
     *     </code>
308
     */
309 234
    public function getParentAssociation(object $document) : ?array
310
    {
311 234
        $oid = spl_object_hash($document);
312
313 234
        return $this->parentAssociations[$oid] ?? null;
314
    }
315
316
    /**
317
     * Get the document persister instance for the given document name
318
     */
319 1234
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
320
    {
321 1234
        if (! isset($this->persisters[$documentName])) {
322 1234
            $class                           = $this->dm->getClassMetadata($documentName);
323 1234
            $pb                              = $this->getPersistenceBuilder();
324 1234
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
325
        }
326
327 1234
        return $this->persisters[$documentName];
328
    }
329
330
    /**
331
     * Get the collection persister instance.
332
     */
333 1234
    public function getCollectionPersister() : CollectionPersister
334
    {
335 1234
        if (! isset($this->collectionPersister)) {
336 1234
            $pb                        = $this->getPersistenceBuilder();
337 1234
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
338
        }
339
340 1234
        return $this->collectionPersister;
341
    }
342
343
    /**
344
     * Set the document persister instance to use for the given document name
345
     *
346
     * @internal
347
     */
348
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
349
    {
350
        $this->persisters[$documentName] = $persister;
351
    }
352
353
    /**
354
     * Commits the UnitOfWork, executing all operations that have been postponed
355
     * up to this point. The state of all managed documents will be synchronized with
356
     * the database.
357
     *
358
     * The operations are executed in the following order:
359
     *
360
     * 1) All document insertions
361
     * 2) All document updates
362
     * 3) All document deletions
363
     *
364
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
365
     */
366 625
    public function commit(array $options = []) : void
367
    {
368
        // Raise preFlush
369 625
        if ($this->evm->hasListeners(Events::preFlush)) {
370
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
371
        }
372
373
        // Compute changes done since last commit.
374 625
        $this->computeChangeSets();
375
376 624
        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...
377 263
            $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...
378 220
            $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...
379 203
            $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...
380 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...
381 22
            $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...
382 624
            $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...
383
        ) {
384 22
            return; // Nothing to do.
385
        }
386
387 621
        $this->commitsInProgress++;
388 621
        if ($this->commitsInProgress > 1) {
389
            throw MongoDBException::commitInProgress();
390
        }
391
        try {
392 621
            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...
393 55
                foreach ($this->orphanRemovals as $removal) {
394 55
                    $this->remove($removal);
395
                }
396
            }
397
398
            // Raise onFlush
399 621
            if ($this->evm->hasListeners(Events::onFlush)) {
400 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
401
            }
402
403 620
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
404 88
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $class does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
405 88
                $this->executeUpserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
406
            }
407
408 620
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
409 543
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
410 543
                $this->executeInserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
411
            }
412
413 607
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
414 241
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
415 241
                $this->executeUpdates($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
416
            }
417
418 607
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
419 79
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
420 79
                $this->executeDeletions($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
421
            }
422
423
            // Raise postFlush
424 607
            if ($this->evm->hasListeners(Events::postFlush)) {
425
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
426
            }
427
428
            // Clear up
429 607
            $this->documentInsertions          =
430 607
            $this->documentUpserts             =
431 607
            $this->documentUpdates             =
432 607
            $this->documentDeletions           =
433 607
            $this->documentChangeSets          =
434 607
            $this->collectionUpdates           =
435 607
            $this->collectionDeletions         =
436 607
            $this->visitedCollections          =
437 607
            $this->scheduledForSynchronization =
438 607
            $this->orphanRemovals              =
439 607
            $this->hasScheduledCollections     = [];
440 607
        } finally {
441 621
            $this->commitsInProgress--;
442
        }
443 607
    }
444
445
    /**
446
     * Groups a list of scheduled documents by their class.
447
     */
448 620
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
449
    {
450 620
        if (empty($documents)) {
451 620
            return [];
452
        }
453
454 619
        $divided = [];
455 619
        $embeds  = [];
456 619
        foreach ($documents as $oid => $d) {
457 619
            $className = get_class($d);
458 619
            if (isset($embeds[$className])) {
459 81
                continue;
460
            }
461
462 619
            if (isset($divided[$className])) {
463 171
                $divided[$className][1][$oid] = $d;
464 171
                continue;
465
            }
466
467 619
            $class = $this->dm->getClassMetadata($className);
468 619
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
469 183
                $embeds[$className] = true;
470 183
                continue;
471
            }
472
473 619
            if ($class->isView()) {
474
                continue;
475
            }
476
477 619
            if (empty($divided[$class->name])) {
478 619
                $divided[$class->name] = [$class, [$oid => $d]];
479
            } else {
480 4
                $divided[$class->name][1][$oid] = $d;
481
            }
482
        }
483
484 619
        return $divided;
485
    }
486
487
    /**
488
     * Compute changesets of all documents scheduled for insertion.
489
     *
490
     * Embedded documents will not be processed.
491
     */
492 630
    private function computeScheduleInsertsChangeSets() : void
493
    {
494 630
        foreach ($this->documentInsertions as $document) {
495 556
            $class = $this->dm->getClassMetadata(get_class($document));
496 556
            if ($class->isEmbeddedDocument || $class->isView()) {
497 165
                continue;
498
            }
499
500 550
            $this->computeChangeSet($class, $document);
501
        }
502 629
    }
503
504
    /**
505
     * Compute changesets of all documents scheduled for upsert.
506
     *
507
     * Embedded documents will not be processed.
508
     */
509 629
    private function computeScheduleUpsertsChangeSets() : void
510
    {
511 629
        foreach ($this->documentUpserts as $document) {
512 87
            $class = $this->dm->getClassMetadata(get_class($document));
513 87
            if ($class->isEmbeddedDocument || $class->isView()) {
514
                continue;
515
            }
516
517 87
            $this->computeChangeSet($class, $document);
518
        }
519 629
    }
520
521
    /**
522
     * Gets the changeset for a document.
523
     *
524
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
525
     */
526 635
    public function getDocumentChangeSet(object $document) : array
527
    {
528 635
        $oid = spl_object_hash($document);
529
530 635
        return $this->documentChangeSets[$oid] ?? [];
531
    }
532
533
    /**
534
     * Sets the changeset for a document.
535
     *
536
     * @internal
537
     */
538 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
539
    {
540 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
541 1
    }
542
543
    /**
544
     * Get a documents actual data, flattening all the objects to arrays.
545
     *
546
     * @internal
547
     *
548
     * @return array
549
     */
550 638
    public function getDocumentActualData(object $document) : array
551
    {
552 638
        $class      = $this->dm->getClassMetadata(get_class($document));
553 638
        $actualData = [];
554 638
        foreach ($class->reflFields as $name => $refProp) {
555 638
            $mapping = $class->fieldMappings[$name];
556
            // skip not saved fields
557 638
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
558 207
                continue;
559
            }
560 638
            $value = $refProp->getValue($document);
561 638
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
562 638
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
563
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
564 413
                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...
565 149
                    $value = new ArrayCollection($value);
566
                }
567
568
                // Inject PersistentCollection
569 413
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
570 413
                $coll->setOwner($document, $mapping);
571 413
                $coll->setDirty(! $value->isEmpty());
572 413
                $class->reflFields[$name]->setValue($document, $coll);
573 413
                $actualData[$name] = $coll;
574
            } else {
575 638
                $actualData[$name] = $value;
576
            }
577
        }
578
579 638
        return $actualData;
580
    }
581
582
    /**
583
     * Computes the changes that happened to a single document.
584
     *
585
     * Modifies/populates the following properties:
586
     *
587
     * {@link originalDocumentData}
588
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
589
     * then it was not fetched from the database and therefore we have no original
590
     * document data yet. All of the current document data is stored as the original document data.
591
     *
592
     * {@link documentChangeSets}
593
     * The changes detected on all properties of the document are stored there.
594
     * A change is a tuple array where the first entry is the old value and the second
595
     * entry is the new value of the property. Changesets are used by persisters
596
     * to INSERT/UPDATE the persistent document state.
597
     *
598
     * {@link documentUpdates}
599
     * If the document is already fully MANAGED (has been fetched from the database before)
600
     * and any changes to its properties are detected, then a reference to the document is stored
601
     * there to mark it for an update.
602
     */
603 634
    public function computeChangeSet(ClassMetadata $class, object $document) : void
604
    {
605 634
        if (! $class->isInheritanceTypeNone()) {
606 192
            $class = $this->dm->getClassMetadata(get_class($document));
607
        }
608
609
        // Fire PreFlush lifecycle callbacks
610 634
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
611 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
612
        }
613
614 634
        $this->computeOrRecomputeChangeSet($class, $document);
615 633
    }
616
617
    /**
618
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
619
     */
620 634
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
621
    {
622 634
        if ($class->isView()) {
623
            return;
624
        }
625
626 634
        $oid           = spl_object_hash($document);
627 634
        $actualData    = $this->getDocumentActualData($document);
628 634
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
629 634
        if ($isNewDocument) {
630
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
631
            // These result in an INSERT.
632 633
            $this->originalDocumentData[$oid] = $actualData;
633 633
            $changeSet                        = [];
634 633
            foreach ($actualData as $propName => $actualValue) {
635
                /* At this PersistentCollection shouldn't be here, probably it
636
                 * was cloned and its ownership must be fixed
637
                 */
638 633
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
639
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
640
                    $actualValue           = $actualData[$propName];
641
                }
642
                // ignore inverse side of reference relationship
643 633
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
644 195
                    continue;
645
                }
646 633
                $changeSet[$propName] = [null, $actualValue];
647
            }
648 633
            $this->documentChangeSets[$oid] = $changeSet;
649
        } else {
650 302
            if ($class->isReadOnly) {
651 2
                return;
652
            }
653
            // Document is "fully" MANAGED: it was already fully persisted before
654
            // and we have a copy of the original data
655 300
            $originalData           = $this->originalDocumentData[$oid];
656 300
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
657 300
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
658 1
                $changeSet = $this->documentChangeSets[$oid];
659
            } else {
660 300
                $changeSet = [];
661
            }
662
663 300
            $gridFSMetadataProperty = null;
664
665 300
            if ($class->isFile) {
666
                try {
667 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
668 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
669 1
                } catch (MappingException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
670
                }
671
            }
672
673 300
            foreach ($actualData as $propName => $actualValue) {
674
                // skip not saved fields
675 300
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
676 300
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
677 4
                    continue;
678
                }
679
680 299
                $orgValue = $originalData[$propName] ?? null;
681
682
                // skip if value has not changed
683 299
                if ($orgValue === $actualValue) {
684 297
                    if (! $actualValue instanceof PersistentCollectionInterface) {
685 297
                        continue;
686
                    }
687
688 211
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
689
                        // consider dirty collections as changed as well
690 188
                        continue;
691
                    }
692
                }
693
694
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
695 259
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
696 14
                    if ($orgValue !== null) {
697 8
                        $this->scheduleOrphanRemoval($orgValue);
698
                    }
699 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
700 14
                    continue;
701
                }
702
703
                // if owning side of reference-one relationship
704 252
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
705 12
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
706 1
                        $this->scheduleOrphanRemoval($orgValue);
707
                    }
708
709 12
                    $changeSet[$propName] = [$orgValue, $actualValue];
710 12
                    continue;
711
                }
712
713 245
                if ($isChangeTrackingNotify) {
714 2
                    continue;
715
                }
716
717
                // ignore inverse side of reference relationship
718 244
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
719 6
                    continue;
720
                }
721
722
                // Persistent collection was exchanged with the "originally"
723
                // created one. This can only mean it was cloned and replaced
724
                // on another document.
725 242
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
726 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
727
                }
728
729
                // if embed-many or reference-many relationship
730 242
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
731 125
                    $changeSet[$propName] = [$orgValue, $actualValue];
732
                    /* If original collection was exchanged with a non-empty value
733
                     * and $set will be issued, there is no need to $unset it first
734
                     */
735 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
736 31
                        continue;
737
                    }
738 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
739 18
                        $this->scheduleCollectionDeletion($orgValue);
740
                    }
741 104
                    continue;
742
                }
743
744
                // skip equivalent date values
745 154
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
746
                    /** @var DateType $dateType */
747 37
                    $dateType      = Type::getType('date');
748 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
749 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
750
751 37
                    $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...
752 37
                    $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...
753
754 37
                    if ($orgTimestamp === $actualTimestamp) {
755 30
                        continue;
756
                    }
757
                }
758
759
                // regular field
760 137
                $changeSet[$propName] = [$orgValue, $actualValue];
761
            }
762 300
            if ($changeSet) {
763 248
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
764 18
                    ? $changeSet + $this->documentChangeSets[$oid]
765 245
                    : $changeSet;
766
767 248
                $this->originalDocumentData[$oid] = $actualData;
768 248
                $this->scheduleForUpdate($document);
769
            }
770
        }
771
772
        // Look for changes in associations of the document
773 634
        $associationMappings = array_filter(
774 634
            $class->associationMappings,
775
            static function ($assoc) {
776 491
                return empty($assoc['notSaved']);
777 634
            }
778
        );
779
780 634
        foreach ($associationMappings as $mapping) {
781 491
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
782
783 491
            if ($value === null) {
784 330
                continue;
785
            }
786
787 473
            $this->computeAssociationChanges($document, $mapping, $value);
788
789 472
            if (isset($mapping['reference'])) {
790 352
                continue;
791
            }
792
793 364
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
794
795 364
            foreach ($values as $obj) {
796 188
                $oid2 = spl_object_hash($obj);
797
798 188
                if (isset($this->documentChangeSets[$oid2])) {
799 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
800
                        // instance of $value is the same as it was previously otherwise there would be
801
                        // change set already in place
802 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
803
                    }
804
805 186
                    if (! $isNewDocument) {
806 87
                        $this->scheduleForUpdate($document);
807
                    }
808
809 186
                    break;
810
                }
811
            }
812
        }
813 633
    }
814
815
    /**
816
     * Computes all the changes that have been done to documents and collections
817
     * since the last commit and stores these changes in the _documentChangeSet map
818
     * temporarily for access by the persisters, until the UoW commit is finished.
819
     */
820 630
    public function computeChangeSets() : void
821
    {
822 630
        $this->computeScheduleInsertsChangeSets();
823 629
        $this->computeScheduleUpsertsChangeSets();
824
825
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
826 629
        foreach ($this->identityMap as $className => $documents) {
827 629
            $class = $this->dm->getClassMetadata($className);
828 629
            if ($class->isEmbeddedDocument || $class->isView()) {
829
                /* we do not want to compute changes to embedded documents up front
830
                 * in case embedded document was replaced and its changeset
831
                 * would corrupt data. Embedded documents' change set will
832
                 * be calculated by reachability from owning document.
833
                 */
834 179
                continue;
835
            }
836
837
            // If change tracking is explicit or happens through notification, then only compute
838
            // changes on document of that type that are explicitly marked for synchronization.
839
            switch (true) {
840 629
                case $class->isChangeTrackingDeferredImplicit():
841 628
                    $documentsToProcess = $documents;
842 628
                    break;
843
844 3
                case isset($this->scheduledForSynchronization[$className]):
845 2
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
846 2
                    break;
847
848
                default:
849 3
                    $documentsToProcess = [];
850
            }
851
852 629
            foreach ($documentsToProcess as $document) {
853
                // Ignore uninitialized proxy objects
854 624
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
855 11
                    continue;
856
                }
857
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
858 624
                $oid = spl_object_hash($document);
859 624
                if (isset($this->documentInsertions[$oid])
860 343
                    || isset($this->documentUpserts[$oid])
861 297
                    || isset($this->documentDeletions[$oid])
862 624
                    || ! isset($this->documentStates[$oid])
863
                ) {
864 622
                    continue;
865
                }
866
867 297
                $this->computeChangeSet($class, $document);
868
            }
869
        }
870 629
    }
871
872
    /**
873
     * Computes the changes of an association.
874
     *
875
     * @param mixed $value The value of the association.
876
     *
877
     * @throws InvalidArgumentException
878
     */
879 473
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
880
    {
881 473
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
882 473
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
883 473
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
884
885 473
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
886 7
            return;
887
        }
888
889 472
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
890 260
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
891 256
                $this->scheduleCollectionUpdate($value);
892
            }
893
894 260
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
895 260
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
896 260
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
897 151
                $value->initialize();
898 151
                foreach ($value->getDeletedDocuments() as $orphan) {
899 25
                    $this->scheduleOrphanRemoval($orphan);
900
                }
901
            }
902
        }
903
904
        // Look through the documents, and in any of their associations,
905
        // for transient (new) documents, recursively. ("Persistence by reachability")
906
        // Unwrap. Uninitialized collections will simply be empty.
907 472
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
908
909 472
        $count = 0;
910 472
        foreach ($unwrappedValue as $key => $entry) {
911 378
            if (! is_object($entry)) {
912 1
                throw new InvalidArgumentException(
913 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
914
                );
915
            }
916
917 377
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
918
919 377
            $state = $this->getDocumentState($entry, self::STATE_NEW);
920
921
            // Handle "set" strategy for multi-level hierarchy
922 377
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
923 377
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
924
925 377
            $count++;
926
927
            switch ($state) {
928 377
                case self::STATE_NEW:
929 70
                    if (! $assoc['isCascadePersist']) {
930
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
931
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
932
                            . ' Explicitly persist the new document or configure cascading persist operations'
933
                            . ' on the relationship.');
934
                    }
935
936 70
                    $this->persistNew($targetClass, $entry);
937 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
938 70
                    $this->computeChangeSet($targetClass, $entry);
939 70
                    break;
940
941 373
                case self::STATE_MANAGED:
942 373
                    if ($targetClass->isEmbeddedDocument) {
943 179
                        [, $knownParent ] = $this->getParentAssociation($entry);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
944 179
                        if ($knownParent && $knownParent !== $parentDocument) {
945 6
                            $entry = clone $entry;
946 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
947 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
948 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
949 3
                                $poid = spl_object_hash($parentDocument);
950 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
951 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
952
                                }
953
                            } else {
954
                                // must use unwrapped value to not trigger orphan removal
955 4
                                $unwrappedValue[$key] = $entry;
956
                            }
957 6
                            $this->persistNew($targetClass, $entry);
958
                        }
959 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
960 179
                        $this->computeChangeSet($targetClass, $entry);
961
                    }
962 373
                    break;
963
964 1
                case self::STATE_REMOVED:
965
                    // Consume the $value as array (it's either an array or an ArrayAccess)
966
                    // and remove the element from Collection.
967 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
968
                        unset($value[$key]);
969
                    }
970 1
                    break;
971
972
                case self::STATE_DETACHED:
973
                    // Can actually not happen right now as we assume STATE_NEW,
974
                    // so the exception will be raised from the DBAL layer (constraint violation).
975
                    throw new InvalidArgumentException('A detached document was found through a '
976
                        . 'relationship during cascading a persist operation.');
977
978
                default:
979
                    // MANAGED associated documents are already taken into account
980
                    // during changeset calculation anyway, since they are in the identity map.
981
            }
982
        }
983 471
    }
984
985
    /**
986
     * Computes the changeset of an individual document, independently of the
987
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
988
     *
989
     * The passed document must be a managed document. If the document already has a change set
990
     * because this method is invoked during a commit cycle then the change sets are added.
991
     * whereby changes detected in this method prevail.
992
     *
993
     * @throws InvalidArgumentException If the passed document is not MANAGED.
994
     */
995 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
996
    {
997
        // Ignore uninitialized proxy objects
998 19
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
999 1
            return;
1000
        }
1001
1002 18
        $oid = spl_object_hash($document);
1003
1004 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
1005
            throw new InvalidArgumentException('Document must be managed.');
1006
        }
1007
1008 18
        if (! $class->isInheritanceTypeNone()) {
1009 2
            $class = $this->dm->getClassMetadata(get_class($document));
1010
        }
1011
1012 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1013 18
    }
1014
1015
    /**
1016
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1017
     */
1018 666
    private function persistNew(ClassMetadata $class, object $document) : void
1019
    {
1020 666
        $this->lifecycleEventManager->prePersist($class, $document);
1021 666
        $oid    = spl_object_hash($document);
1022 666
        $upsert = false;
1023 666
        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...
1024 666
            $idValue = $class->getIdentifierValue($document);
1025 666
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1026
1027 666
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1028 3
                throw new InvalidArgumentException(sprintf(
1029 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1030 3
                    get_class($document)
1031
                ));
1032
            }
1033
1034 665
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1035 1
                throw new InvalidArgumentException(sprintf(
1036 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1037 1
                    get_class($document)
1038
                ));
1039
            }
1040
1041 664
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1042 581
                $idValue = $class->idGenerator->generate($this->dm, $document);
1043 581
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1044 581
                $class->setIdentifierValue($document, $idValue);
1045
            }
1046
1047 664
            $this->documentIdentifiers[$oid] = $idValue;
1048
        } else {
1049
            // this is for embedded documents without identifiers
1050 152
            $this->documentIdentifiers[$oid] = $oid;
1051
        }
1052
1053 664
        $this->documentStates[$oid] = self::STATE_MANAGED;
1054
1055 664
        if ($upsert) {
1056 94
            $this->scheduleForUpsert($class, $document);
1057
        } else {
1058 590
            $this->scheduleForInsert($class, $document);
1059
        }
1060 664
    }
1061
1062
    /**
1063
     * Executes all document insertions for documents of the specified type.
1064
     */
1065 543
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1066
    {
1067 543
        $persister = $this->getDocumentPersister($class->name);
1068
1069 543
        foreach ($documents as $oid => $document) {
1070 543
            $persister->addInsert($document);
1071 543
            unset($this->documentInsertions[$oid]);
1072
        }
1073
1074 543
        $persister->executeInserts($options);
1075
1076 532
        foreach ($documents as $document) {
1077 532
            $this->lifecycleEventManager->postPersist($class, $document);
1078
        }
1079 532
    }
1080
1081
    /**
1082
     * Executes all document upserts for documents of the specified type.
1083
     */
1084 88
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1085
    {
1086 88
        $persister = $this->getDocumentPersister($class->name);
1087
1088 88
        foreach ($documents as $oid => $document) {
1089 88
            $persister->addUpsert($document);
1090 88
            unset($this->documentUpserts[$oid]);
1091
        }
1092
1093 88
        $persister->executeUpserts($options);
1094
1095 88
        foreach ($documents as $document) {
1096 88
            $this->lifecycleEventManager->postPersist($class, $document);
1097
        }
1098 88
    }
1099
1100
    /**
1101
     * Executes all document updates for documents of the specified type.
1102
     */
1103 241
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1104
    {
1105 241
        if ($class->isReadOnly) {
1106
            return;
1107
        }
1108
1109 241
        $className = $class->name;
1110 241
        $persister = $this->getDocumentPersister($className);
1111
1112 241
        foreach ($documents as $oid => $document) {
1113 241
            $this->lifecycleEventManager->preUpdate($class, $document);
1114
1115 241
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1116 240
                $persister->update($document, $options);
1117
            }
1118
1119 232
            unset($this->documentUpdates[$oid]);
1120
1121 232
            $this->lifecycleEventManager->postUpdate($class, $document);
1122
        }
1123 231
    }
1124
1125
    /**
1126
     * Executes all document deletions for documents of the specified type.
1127
     */
1128 79
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1129
    {
1130 79
        $persister = $this->getDocumentPersister($class->name);
1131
1132 79
        foreach ($documents as $oid => $document) {
1133 79
            if (! $class->isEmbeddedDocument) {
1134 36
                $persister->delete($document, $options);
1135
            }
1136
            unset(
1137 77
                $this->documentDeletions[$oid],
1138 77
                $this->documentIdentifiers[$oid],
1139 77
                $this->originalDocumentData[$oid]
1140
            );
1141
1142
            // Clear snapshot information for any referenced PersistentCollection
1143
            // http://www.doctrine-project.org/jira/browse/MODM-95
1144 77
            foreach ($class->associationMappings as $fieldMapping) {
1145 53
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1146 38
                    continue;
1147
                }
1148
1149 33
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1150 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1151 7
                    continue;
1152
                }
1153
1154 29
                $value->clearSnapshot();
1155
            }
1156
1157
            // Document with this $oid after deletion treated as NEW, even if the $oid
1158
            // is obtained by a new document because the old one went out of scope.
1159 77
            $this->documentStates[$oid] = self::STATE_NEW;
1160
1161 77
            $this->lifecycleEventManager->postRemove($class, $document);
1162
        }
1163 77
    }
1164
1165
    /**
1166
     * Schedules a document for insertion into the database.
1167
     * If the document already has an identifier, it will be added to the
1168
     * identity map.
1169
     *
1170
     * @internal
1171
     *
1172
     * @throws InvalidArgumentException
1173
     */
1174 593
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1175
    {
1176 593
        $oid = spl_object_hash($document);
1177
1178 593
        if (isset($this->documentUpdates[$oid])) {
1179
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1180
        }
1181 593
        if (isset($this->documentDeletions[$oid])) {
1182
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1183
        }
1184 593
        if (isset($this->documentInsertions[$oid])) {
1185
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1186
        }
1187
1188 593
        $this->documentInsertions[$oid] = $document;
1189
1190 593
        if (! isset($this->documentIdentifiers[$oid])) {
1191 3
            return;
1192
        }
1193
1194 590
        $this->addToIdentityMap($document);
1195 590
    }
1196
1197
    /**
1198
     * Schedules a document for upsert into the database and adds it to the
1199
     * identity map
1200
     *
1201
     * @internal
1202
     *
1203
     * @throws InvalidArgumentException
1204
     */
1205 97
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1206
    {
1207 97
        $oid = spl_object_hash($document);
1208
1209 97
        if ($class->isEmbeddedDocument) {
1210
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1211
        }
1212 97
        if (isset($this->documentUpdates[$oid])) {
1213
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1214
        }
1215 97
        if (isset($this->documentDeletions[$oid])) {
1216
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1217
        }
1218 97
        if (isset($this->documentUpserts[$oid])) {
1219
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1220
        }
1221
1222 97
        $this->documentUpserts[$oid]     = $document;
1223 97
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1224 97
        $this->addToIdentityMap($document);
1225 97
    }
1226
1227
    /**
1228
     * Checks whether a document is scheduled for insertion.
1229
     */
1230 109
    public function isScheduledForInsert(object $document) : bool
1231
    {
1232 109
        return isset($this->documentInsertions[spl_object_hash($document)]);
1233
    }
1234
1235
    /**
1236
     * Checks whether a document is scheduled for upsert.
1237
     */
1238 5
    public function isScheduledForUpsert(object $document) : bool
1239
    {
1240 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1241
    }
1242
1243
    /**
1244
     * Schedules a document for being updated.
1245
     *
1246
     * @internal
1247
     *
1248
     * @throws InvalidArgumentException
1249
     */
1250 250
    public function scheduleForUpdate(object $document) : void
1251
    {
1252 250
        $oid = spl_object_hash($document);
1253 250
        if (! isset($this->documentIdentifiers[$oid])) {
1254
            throw new InvalidArgumentException('Document has no identity.');
1255
        }
1256
1257 250
        if (isset($this->documentDeletions[$oid])) {
1258
            throw new InvalidArgumentException('Document is removed.');
1259
        }
1260
1261 250
        if (isset($this->documentUpdates[$oid])
1262 250
            || isset($this->documentInsertions[$oid])
1263 250
            || isset($this->documentUpserts[$oid])) {
1264 104
            return;
1265
        }
1266
1267 248
        $this->documentUpdates[$oid] = $document;
1268 248
    }
1269
1270
    /**
1271
     * Checks whether a document is registered as dirty in the unit of work.
1272
     * Note: Is not very useful currently as dirty documents are only registered
1273
     * at commit time.
1274
     */
1275 21
    public function isScheduledForUpdate(object $document) : bool
1276
    {
1277 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1278
    }
1279
1280
    /**
1281
     * Checks whether a document is registered to be checked in the unit of work.
1282
     */
1283
    public function isScheduledForSynchronization(object $document) : bool
1284
    {
1285
        $class = $this->dm->getClassMetadata(get_class($document));
1286
1287
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1288
    }
1289
1290
    /**
1291
     * Schedules a document for deletion.
1292
     *
1293
     * @internal
1294
     */
1295 84
    public function scheduleForDelete(object $document, bool $isView = false) : void
1296
    {
1297 84
        $oid = spl_object_hash($document);
1298
1299 84
        if (isset($this->documentInsertions[$oid])) {
1300 2
            if ($this->isInIdentityMap($document)) {
1301 2
                $this->removeFromIdentityMap($document);
1302
            }
1303 2
            unset($this->documentInsertions[$oid]);
1304
1305 2
            return; // document has not been persisted yet, so nothing more to do.
1306
        }
1307
1308 83
        if (! $this->isInIdentityMap($document)) {
1309 2
            return; // ignore
1310
        }
1311
1312 82
        $this->removeFromIdentityMap($document);
1313 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1314
1315 82
        if (isset($this->documentUpdates[$oid])) {
1316
            unset($this->documentUpdates[$oid]);
1317
        }
1318 82
        if (isset($this->documentDeletions[$oid])) {
1319
            return;
1320
        }
1321
1322 82
        if ($isView) {
1323
            return;
1324
        }
1325
1326 82
        $this->documentDeletions[$oid] = $document;
1327 82
    }
1328
1329
    /**
1330
     * Checks whether a document is registered as removed/deleted with the unit
1331
     * of work.
1332
     */
1333 5
    public function isScheduledForDelete(object $document) : bool
1334
    {
1335 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1336
    }
1337
1338
    /**
1339
     * Checks whether a document is scheduled for insertion, update or deletion.
1340
     *
1341
     * @internal
1342
     */
1343 259
    public function isDocumentScheduled(object $document) : bool
1344
    {
1345 259
        $oid = spl_object_hash($document);
1346
1347 259
        return isset($this->documentInsertions[$oid]) ||
1348 139
            isset($this->documentUpserts[$oid]) ||
1349 128
            isset($this->documentUpdates[$oid]) ||
1350 259
            isset($this->documentDeletions[$oid]);
1351
    }
1352
1353
    /**
1354
     * Registers a document in the identity map.
1355
     *
1356
     * Note that documents in a hierarchy are registered with the class name of
1357
     * the root document. Identifiers are serialized before being used as array
1358
     * keys to allow differentiation of equal, but not identical, values.
1359
     *
1360
     * @internal
1361
     */
1362 709
    public function addToIdentityMap(object $document) : bool
1363
    {
1364 709
        $class = $this->dm->getClassMetadata(get_class($document));
1365 709
        $id    = $this->getIdForIdentityMap($document);
1366
1367 709
        if (isset($this->identityMap[$class->name][$id])) {
1368 47
            return false;
1369
        }
1370
1371 709
        $this->identityMap[$class->name][$id] = $document;
1372
1373 709
        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...
1374 709
            ( ! $document instanceof GhostObjectInterface || $document->isProxyInitialized())) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
1375 2
            $document->addPropertyChangedListener($this);
1376
        }
1377
1378 709
        return true;
1379
    }
1380
1381
    /**
1382
     * Gets the state of a document with regard to the current unit of work.
1383
     *
1384
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1385
     *                         This parameter can be set to improve performance of document state detection
1386
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1387
     *                         is either known or does not matter for the caller of the method.
1388
     */
1389 669
    public function getDocumentState(object $document, ?int $assume = null) : int
1390
    {
1391 669
        $oid = spl_object_hash($document);
1392
1393 669
        if (isset($this->documentStates[$oid])) {
1394 417
            return $this->documentStates[$oid];
1395
        }
1396
1397 668
        $class = $this->dm->getClassMetadata(get_class($document));
1398
1399 668
        if ($class->isEmbeddedDocument) {
1400 192
            return self::STATE_NEW;
1401
        }
1402
1403 665
        if ($assume !== null) {
1404 664
            return $assume;
1405
        }
1406
1407
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1408
         * known. Note that you cannot remember the NEW or DETACHED state in
1409
         * _documentStates since the UoW does not hold references to such
1410
         * objects and the object hash can be reused. More generally, because
1411
         * the state may "change" between NEW/DETACHED without the UoW being
1412
         * aware of it.
1413
         */
1414 3
        $id = $class->getIdentifierObject($document);
1415
1416 3
        if ($id === null) {
1417 2
            return self::STATE_NEW;
1418
        }
1419
1420
        // Check for a version field, if available, to avoid a DB lookup.
1421 2
        if ($class->isVersioned && $class->versionField !== null) {
1422
            return $class->getFieldValue($document, $class->versionField)
1423
                ? self::STATE_DETACHED
1424
                : self::STATE_NEW;
1425
        }
1426
1427
        // Last try before DB lookup: check the identity map.
1428 2
        if ($this->tryGetById($id, $class)) {
1429 1
            return self::STATE_DETACHED;
1430
        }
1431
1432
        // DB lookup
1433 1
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1434 1
            return self::STATE_DETACHED;
1435
        }
1436
1437
        return self::STATE_NEW;
1438
    }
1439
1440
    /**
1441
     * Removes a document from the identity map. This effectively detaches the
1442
     * document from the persistence management of Doctrine.
1443
     *
1444
     * @internal
1445
     *
1446
     * @throws InvalidArgumentException
1447
     */
1448 97
    public function removeFromIdentityMap(object $document) : bool
1449
    {
1450 97
        $oid = spl_object_hash($document);
1451
1452
        // Check if id is registered first
1453 97
        if (! isset($this->documentIdentifiers[$oid])) {
1454
            return false;
1455
        }
1456
1457 97
        $class = $this->dm->getClassMetadata(get_class($document));
1458 97
        $id    = $this->getIdForIdentityMap($document);
1459
1460 97
        if (isset($this->identityMap[$class->name][$id])) {
1461 97
            unset($this->identityMap[$class->name][$id]);
1462 97
            $this->documentStates[$oid] = self::STATE_DETACHED;
1463
1464 97
            return true;
1465
        }
1466
1467
        return false;
1468
    }
1469
1470
    /**
1471
     * Gets a document in the identity map by its identifier hash.
1472
     *
1473
     * @internal
1474
     *
1475
     * @param mixed $id Document identifier
1476
     *
1477
     * @throws InvalidArgumentException If the class does not have an identifier.
1478
     */
1479 38
    public function getById($id, ClassMetadata $class) : object
1480
    {
1481 38
        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...
1482
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1483
        }
1484
1485 38
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1486
1487 38
        return $this->identityMap[$class->name][$serializedId];
1488
    }
1489
1490
    /**
1491
     * Tries to get a document by its identifier hash. If no document is found
1492
     * for the given hash, FALSE is returned.
1493
     *
1494
     * @internal
1495
     *
1496
     * @param mixed $id Document identifier
1497
     *
1498
     * @return mixed The found document or FALSE.
1499
     *
1500
     * @throws InvalidArgumentException If the class does not have an identifier.
1501
     */
1502 315
    public function tryGetById($id, ClassMetadata $class)
1503
    {
1504 315
        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...
1505
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1506
        }
1507
1508 315
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1509
1510 315
        return $this->identityMap[$class->name][$serializedId] ?? false;
1511
    }
1512
1513
    /**
1514
     * Schedules a document for dirty-checking at commit-time.
1515
     *
1516
     * @internal
1517
     */
1518 2
    public function scheduleForSynchronization(object $document) : void
1519
    {
1520 2
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1521 2
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1522 2
    }
1523
1524
    /**
1525
     * Checks whether a document is registered in the identity map.
1526
     *
1527
     * @internal
1528
     */
1529 90
    public function isInIdentityMap(object $document) : bool
1530
    {
1531 90
        $oid = spl_object_hash($document);
1532
1533 90
        if (! isset($this->documentIdentifiers[$oid])) {
1534 6
            return false;
1535
        }
1536
1537 88
        $class = $this->dm->getClassMetadata(get_class($document));
1538 88
        $id    = $this->getIdForIdentityMap($document);
1539
1540 88
        return isset($this->identityMap[$class->name][$id]);
1541
    }
1542
1543 709
    private function getIdForIdentityMap(object $document) : string
1544
    {
1545 709
        $class = $this->dm->getClassMetadata(get_class($document));
1546
1547 709
        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...
1548 158
            $id = spl_object_hash($document);
1549
        } else {
1550 708
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1551 708
            $id = serialize($class->getDatabaseIdentifierValue($id));
1552
        }
1553
1554 709
        return $id;
1555
    }
1556
1557
    /**
1558
     * Checks whether an identifier exists in the identity map.
1559
     *
1560
     * @internal
1561
     */
1562
    public function containsId($id, string $rootClassName) : bool
1563
    {
1564
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1565
    }
1566
1567
    /**
1568
     * Persists a document as part of the current unit of work.
1569
     *
1570
     * @internal
1571
     *
1572
     * @throws MongoDBException If trying to persist MappedSuperclass.
1573
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1574
     */
1575 667
    public function persist(object $document) : void
1576
    {
1577 667
        $class = $this->dm->getClassMetadata(get_class($document));
1578 667
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1579 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1580
        }
1581 666
        $visited = [];
1582 666
        $this->doPersist($document, $visited);
1583 661
    }
1584
1585
    /**
1586
     * Saves a document as part of the current unit of work.
1587
     * This method is internally called during save() cascades as it tracks
1588
     * the already visited documents to prevent infinite recursions.
1589
     *
1590
     * NOTE: This method always considers documents that are not yet known to
1591
     * this UnitOfWork as NEW.
1592
     *
1593
     * @throws InvalidArgumentException
1594
     * @throws MongoDBException
1595
     */
1596 666
    private function doPersist(object $document, array &$visited) : void
1597
    {
1598 666
        $oid = spl_object_hash($document);
1599 666
        if (isset($visited[$oid])) {
1600 26
            return; // Prevent infinite recursion
1601
        }
1602
1603 666
        $visited[$oid] = $document; // Mark visited
1604
1605 666
        $class = $this->dm->getClassMetadata(get_class($document));
1606
1607 666
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1608
        switch ($documentState) {
1609 666
            case self::STATE_MANAGED:
1610
                // Nothing to do, except if policy is "deferred explicit"
1611 58
                if ($class->isChangeTrackingDeferredExplicit() && ! $class->isView()) {
1612
                    $this->scheduleForSynchronization($document);
1613
                }
1614 58
                break;
1615 666
            case self::STATE_NEW:
1616 666
                if ($class->isFile) {
1617 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1618
                }
1619
1620 665
                if ($class->isView()) {
1621
                    return;
1622
                }
1623
1624 665
                $this->persistNew($class, $document);
1625 663
                break;
1626
1627 2
            case self::STATE_REMOVED:
1628
                // Document becomes managed again
1629 2
                unset($this->documentDeletions[$oid]);
1630
1631 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1632 2
                break;
1633
1634
            case self::STATE_DETACHED:
1635
                throw new InvalidArgumentException(
1636
                    'Behavior of persist() for a detached document is not yet defined.'
1637
                );
1638
1639
            default:
1640
                throw MongoDBException::invalidDocumentState($documentState);
1641
        }
1642
1643 663
        $this->cascadePersist($document, $visited);
1644 661
    }
1645
1646
    /**
1647
     * Deletes a document as part of the current unit of work.
1648
     *
1649
     * @internal
1650
     */
1651 83
    public function remove(object $document)
1652
    {
1653 83
        $visited = [];
1654 83
        $this->doRemove($document, $visited);
1655 83
    }
1656
1657
    /**
1658
     * Deletes a document as part of the current unit of work.
1659
     *
1660
     * This method is internally called during delete() cascades as it tracks
1661
     * the already visited documents to prevent infinite recursions.
1662
     *
1663
     * @throws MongoDBException
1664
     */
1665 83
    private function doRemove(object $document, array &$visited) : void
1666
    {
1667 83
        $oid = spl_object_hash($document);
1668 83
        if (isset($visited[$oid])) {
1669 1
            return; // Prevent infinite recursion
1670
        }
1671
1672 83
        $visited[$oid] = $document; // mark visited
1673
1674
        /* Cascade first, because scheduleForDelete() removes the entity from
1675
         * the identity map, which can cause problems when a lazy Proxy has to
1676
         * be initialized for the cascade operation.
1677
         */
1678 83
        $this->cascadeRemove($document, $visited);
1679
1680 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1681 83
        $documentState = $this->getDocumentState($document);
1682
        switch ($documentState) {
1683 83
            case self::STATE_NEW:
1684 83
            case self::STATE_REMOVED:
1685
                // nothing to do
1686 1
                break;
1687 83
            case self::STATE_MANAGED:
1688 83
                $this->lifecycleEventManager->preRemove($class, $document);
1689 83
                $this->scheduleForDelete($document, $class->isView());
1690 83
                break;
1691
            case self::STATE_DETACHED:
1692
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1693
            default:
1694
                throw MongoDBException::invalidDocumentState($documentState);
1695
        }
1696 83
    }
1697
1698
    /**
1699
     * Merges the state of the given detached document into this UnitOfWork.
1700
     *
1701
     * @internal
1702
     */
1703 12
    public function merge(object $document) : object
1704
    {
1705 12
        $visited = [];
1706
1707 12
        return $this->doMerge($document, $visited);
1708
    }
1709
1710
    /**
1711
     * Executes a merge operation on a document.
1712
     *
1713
     * @throws InvalidArgumentException If the entity instance is NEW.
1714
     * @throws LockException If the document uses optimistic locking through a
1715
     *                       version attribute and the version check against the
1716
     *                       managed copy fails.
1717
     */
1718 12
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1719
    {
1720 12
        $oid = spl_object_hash($document);
1721
1722 12
        if (isset($visited[$oid])) {
1723 1
            return $visited[$oid]; // Prevent infinite recursion
1724
        }
1725
1726 12
        $visited[$oid] = $document; // mark visited
1727
1728 12
        $class = $this->dm->getClassMetadata(get_class($document));
1729
1730
        /* First we assume DETACHED, although it can still be NEW but we can
1731
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1732
         * an identity, we need to fetch it from the DB anyway in order to
1733
         * merge. MANAGED documents are ignored by the merge operation.
1734
         */
1735 12
        $managedCopy = $document;
1736
1737 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1738 12
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
1739
                $document->initializeProxy();
1740
            }
1741
1742 12
            $identifier = $class->getIdentifier();
1743
            // We always have one element in the identifier array but it might be null
1744 12
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1745 12
            $managedCopy = null;
1746
1747
            // Try to fetch document from the database
1748 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1749 12
                $managedCopy = $this->dm->find($class->name, $id);
1750
1751
                // Managed copy may be removed in which case we can't merge
1752 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1753
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1754
                }
1755
1756 12
                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
1757
                    $managedCopy->initializeProxy();
1758
                }
1759
            }
1760
1761 12
            if ($managedCopy === null) {
1762
                // Create a new managed instance
1763 4
                $managedCopy = $class->newInstance();
1764 4
                if ($id !== null) {
1765 3
                    $class->setIdentifierValue($managedCopy, $id);
1766
                }
1767 4
                $this->persistNew($class, $managedCopy);
1768
            }
1769
1770 12
            if ($class->isVersioned) {
1771
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1772
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1773
1774
                // Throw exception if versions don't match
1775
                if ($managedCopyVersion !== $documentVersion) {
1776
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1777
                }
1778
            }
1779
1780
            // Merge state of $document into existing (managed) document
1781 12
            foreach ($class->reflClass->getProperties() as $prop) {
1782 12
                $name = $prop->name;
1783 12
                $prop->setAccessible(true);
1784 12
                if (! isset($class->associationMappings[$name])) {
1785 12
                    if (! $class->isIdentifier($name)) {
1786 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1787
                    }
1788
                } else {
1789 12
                    $assoc2 = $class->associationMappings[$name];
1790
1791 12
                    if ($assoc2['type'] === 'one') {
1792 6
                        $other = $prop->getValue($document);
1793
1794 6
                        if ($other === null) {
1795 2
                            $prop->setValue($managedCopy, null);
1796 5
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
1797
                            // Do not merge fields marked lazy that have not been fetched
1798
                            continue;
1799 5
                        } elseif (! $assoc2['isCascadeMerge']) {
1800 1
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1801 1
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1802
                                /** @var ClassMetadata $targetClass */
1803 1
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1804 1
                                $relatedId   = $targetClass->getIdentifierObject($other);
1805
1806 1
                                $current = $prop->getValue($managedCopy);
1807 1
                                if ($current !== null) {
1808 1
                                    $this->removeFromIdentityMap($current);
1809
                                }
1810
1811 1
                                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...
1812
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1813
                                } else {
1814
                                    $other = $this
1815 1
                                        ->dm
1816 1
                                        ->getProxyFactory()
1817 1
                                        ->getProxy($targetClass, $relatedId);
1818 1
                                    $this->registerManaged($other, $relatedId, []);
1819
                                }
1820
                            }
1821
1822 6
                            $prop->setValue($managedCopy, $other);
1823
                        }
1824
                    } else {
1825 11
                        $mergeCol = $prop->getValue($document);
1826
1827 11
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1828
                            /* Do not merge fields marked lazy that have not
1829
                             * been fetched. Keep the lazy persistent collection
1830
                             * of the managed copy.
1831
                             */
1832 4
                            continue;
1833
                        }
1834
1835 10
                        $managedCol = $prop->getValue($managedCopy);
1836
1837 10
                        if (! $managedCol) {
1838 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1839 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1840 1
                            $prop->setValue($managedCopy, $managedCol);
1841 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1842
                        }
1843
1844
                        /* Note: do not process association's target documents.
1845
                         * They will be handled during the cascade. Initialize
1846
                         * and, if necessary, clear $managedCol for now.
1847
                         */
1848 10
                        if ($assoc2['isCascadeMerge']) {
1849 10
                            $managedCol->initialize();
1850
1851
                            // If $managedCol differs from the merged collection, clear and set dirty
1852 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1853 3
                                $managedCol->unwrap()->clear();
1854 3
                                $managedCol->setDirty(true);
1855
1856 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1857
                                    $this->scheduleForSynchronization($managedCopy);
1858
                                }
1859
                            }
1860
                        }
1861
                    }
1862
                }
1863
1864 12
                if (! $class->isChangeTrackingNotify()) {
1865 12
                    continue;
1866
                }
1867
1868
                // Just treat all properties as changed, there is no other choice.
1869
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1870
            }
1871
1872 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1873
                $this->scheduleForSynchronization($document);
1874
            }
1875
        }
1876
1877 12
        if ($prevManagedCopy !== null) {
1878 5
            $assocField = $assoc['fieldName'];
1879 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1880
1881 5
            if ($assoc['type'] === 'one') {
1882 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1883
            } else {
1884 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1885
1886 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1887 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1888
                }
1889
            }
1890
        }
1891
1892
        // Mark the managed copy visited as well
1893 12
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1894
1895 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1896
1897 12
        return $managedCopy;
1898
    }
1899
1900
    /**
1901
     * Detaches a document from the persistence management. It's persistence will
1902
     * no longer be managed by Doctrine.
1903
     *
1904
     * @internal
1905
     */
1906 11
    public function detach(object $document) : void
1907
    {
1908 11
        $visited = [];
1909 11
        $this->doDetach($document, $visited);
1910 11
    }
1911
1912
    /**
1913
     * Executes a detach operation on the given document.
1914
     */
1915 17
    private function doDetach(object $document, array &$visited) : void
1916
    {
1917 17
        $oid = spl_object_hash($document);
1918 17
        if (isset($visited[$oid])) {
1919 3
            return; // Prevent infinite recursion
1920
        }
1921
1922 17
        $visited[$oid] = $document; // mark visited
1923
1924 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1925 17
            case self::STATE_MANAGED:
1926 17
                $this->removeFromIdentityMap($document);
1927
                unset(
1928 17
                    $this->documentInsertions[$oid],
1929 17
                    $this->documentUpdates[$oid],
1930 17
                    $this->documentDeletions[$oid],
1931 17
                    $this->documentIdentifiers[$oid],
1932 17
                    $this->documentStates[$oid],
1933 17
                    $this->originalDocumentData[$oid],
1934 17
                    $this->parentAssociations[$oid],
1935 17
                    $this->documentUpserts[$oid],
1936 17
                    $this->hasScheduledCollections[$oid],
1937 17
                    $this->embeddedDocumentsRegistry[$oid]
1938
                );
1939 17
                break;
1940 3
            case self::STATE_NEW:
1941 3
            case self::STATE_DETACHED:
1942 3
                return;
1943
        }
1944
1945 17
        $this->cascadeDetach($document, $visited);
1946 17
    }
1947
1948
    /**
1949
     * Refreshes the state of the given document from the database, overwriting
1950
     * any local, unpersisted changes.
1951
     *
1952
     * @internal
1953
     *
1954
     * @throws InvalidArgumentException If the document is not MANAGED.
1955
     */
1956 24
    public function refresh(object $document) : void
1957
    {
1958 24
        $visited = [];
1959 24
        $this->doRefresh($document, $visited);
1960 23
    }
1961
1962
    /**
1963
     * Executes a refresh operation on a document.
1964
     *
1965
     * @throws InvalidArgumentException If the document is not MANAGED.
1966
     */
1967 24
    private function doRefresh(object $document, array &$visited) : void
1968
    {
1969 24
        $oid = spl_object_hash($document);
1970 24
        if (isset($visited[$oid])) {
1971
            return; // Prevent infinite recursion
1972
        }
1973
1974 24
        $visited[$oid] = $document; // mark visited
1975
1976 24
        $class = $this->dm->getClassMetadata(get_class($document));
1977
1978 24
        if (! $class->isEmbeddedDocument) {
1979 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1980 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1981
            }
1982
1983 23
            $this->getDocumentPersister($class->name)->refresh($document);
1984
        }
1985
1986 23
        $this->cascadeRefresh($document, $visited);
1987 23
    }
1988
1989
    /**
1990
     * Cascades a refresh operation to associated documents.
1991
     */
1992 23
    private function cascadeRefresh(object $document, array &$visited) : void
1993
    {
1994 23
        $class = $this->dm->getClassMetadata(get_class($document));
1995
1996 23
        $associationMappings = array_filter(
1997 23
            $class->associationMappings,
1998
            static function ($assoc) {
1999 18
                return $assoc['isCascadeRefresh'];
2000 23
            }
2001
        );
2002
2003 23
        foreach ($associationMappings as $mapping) {
2004 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2005 15
            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...
2006 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2007
                    // Unwrap so that foreach() does not initialize
2008 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2009
                }
2010 15
                foreach ($relatedDocuments as $relatedDocument) {
2011
                    $this->doRefresh($relatedDocument, $visited);
2012
                }
2013 10
            } elseif ($relatedDocuments !== null) {
2014 2
                $this->doRefresh($relatedDocuments, $visited);
2015
            }
2016
        }
2017 23
    }
2018
2019
    /**
2020
     * Cascades a detach operation to associated documents.
2021
     */
2022 17
    private function cascadeDetach(object $document, array &$visited) : void
2023
    {
2024 17
        $class = $this->dm->getClassMetadata(get_class($document));
2025 17
        foreach ($class->fieldMappings as $mapping) {
2026 17
            if (! $mapping['isCascadeDetach']) {
2027 17
                continue;
2028
            }
2029 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2030 11
            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...
2031 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2032
                    // Unwrap so that foreach() does not initialize
2033 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2034
                }
2035 11
                foreach ($relatedDocuments as $relatedDocument) {
2036 8
                    $this->doDetach($relatedDocument, $visited);
2037
                }
2038 11
            } elseif ($relatedDocuments !== null) {
2039 8
                $this->doDetach($relatedDocuments, $visited);
2040
            }
2041
        }
2042 17
    }
2043
2044
    /**
2045
     * Cascades a merge operation to associated documents.
2046
     */
2047 12
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2048
    {
2049 12
        $class = $this->dm->getClassMetadata(get_class($document));
2050
2051 12
        $associationMappings = array_filter(
2052 12
            $class->associationMappings,
2053
            static function ($assoc) {
2054 12
                return $assoc['isCascadeMerge'];
2055 12
            }
2056
        );
2057
2058 12
        foreach ($associationMappings as $assoc) {
2059 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2060
2061 11
            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...
2062 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2063
                    // Collections are the same, so there is nothing to do
2064 1
                    continue;
2065
                }
2066
2067 8
                foreach ($relatedDocuments as $relatedDocument) {
2068 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2069
                }
2070 6
            } elseif ($relatedDocuments !== null) {
2071 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2072
            }
2073
        }
2074 12
    }
2075
2076
    /**
2077
     * Cascades the save operation to associated documents.
2078
     */
2079 663
    private function cascadePersist(object $document, array &$visited) : void
2080
    {
2081 663
        $class = $this->dm->getClassMetadata(get_class($document));
2082
2083 663
        $associationMappings = array_filter(
2084 663
            $class->associationMappings,
2085
            static function ($assoc) {
2086 516
                return $assoc['isCascadePersist'];
2087 663
            }
2088
        );
2089
2090 663
        foreach ($associationMappings as $fieldName => $mapping) {
2091 464
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2092
2093 464
            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...
2094 384
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2095 15
                    if ($relatedDocuments->getOwner() !== $document) {
2096 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2097
                    }
2098
                    // Unwrap so that foreach() does not initialize
2099 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2100
                }
2101
2102 384
                $count = 0;
2103 384
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2104 202
                    if (! empty($mapping['embedded'])) {
2105 130
                        [, $knownParent ] = $this->getParentAssociation($relatedDocument);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
2106 130
                        if ($knownParent && $knownParent !== $document) {
2107 1
                            $relatedDocument               = clone $relatedDocument;
2108 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2109
                        }
2110 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2111 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2112
                    }
2113 202
                    $this->doPersist($relatedDocument, $visited);
2114
                }
2115 361
            } elseif ($relatedDocuments !== null) {
2116 137
                if (! empty($mapping['embedded'])) {
2117 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2118 69
                    if ($knownParent && $knownParent !== $document) {
2119 3
                        $relatedDocuments = clone $relatedDocuments;
2120 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2121
                    }
2122 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2123
                }
2124 137
                $this->doPersist($relatedDocuments, $visited);
2125
            }
2126
        }
2127 661
    }
2128
2129
    /**
2130
     * Cascades the delete operation to associated documents.
2131
     */
2132 83
    private function cascadeRemove(object $document, array &$visited) : void
2133
    {
2134 83
        $class = $this->dm->getClassMetadata(get_class($document));
2135 83
        foreach ($class->fieldMappings as $mapping) {
2136 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2137 82
                continue;
2138
            }
2139 43
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
2140 3
                $document->initializeProxy();
2141
            }
2142
2143 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2144 43
            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...
2145
                // If its a PersistentCollection initialization is intended! No unwrap!
2146 31
                foreach ($relatedDocuments as $relatedDocument) {
2147 15
                    $this->doRemove($relatedDocument, $visited);
2148
                }
2149 27
            } elseif ($relatedDocuments !== null) {
2150 14
                $this->doRemove($relatedDocuments, $visited);
2151
            }
2152
        }
2153 83
    }
2154
2155
    /**
2156
     * Acquire a lock on the given document.
2157
     *
2158
     * @internal
2159
     *
2160
     * @throws LockException
2161
     * @throws InvalidArgumentException
2162
     */
2163 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2164
    {
2165 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2166 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2167
        }
2168
2169 7
        $documentName = get_class($document);
2170 7
        $class        = $this->dm->getClassMetadata($documentName);
2171
2172 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2173 2
            if (! $class->isVersioned) {
2174 1
                throw LockException::notVersioned($documentName);
2175
            }
2176
2177 1
            if ($lockVersion !== null) {
2178 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2179 1
                if ($documentVersion !== $lockVersion) {
2180 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2181
                }
2182
            }
2183 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2184 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2185
        }
2186 5
    }
2187
2188
    /**
2189
     * Releases a lock on the given document.
2190
     *
2191
     * @internal
2192
     *
2193
     * @throws InvalidArgumentException
2194
     */
2195 1
    public function unlock(object $document) : void
2196
    {
2197 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2198
            throw new InvalidArgumentException('Document is not MANAGED.');
2199
        }
2200 1
        $documentName = get_class($document);
2201 1
        $this->getDocumentPersister($documentName)->unlock($document);
2202 1
    }
2203
2204
    /**
2205
     * Clears the UnitOfWork.
2206
     *
2207
     * @internal
2208
     */
2209 385
    public function clear(?string $documentName = null) : void
2210
    {
2211 385
        if ($documentName === null) {
2212 379
            $this->identityMap                 =
2213 379
            $this->documentIdentifiers         =
2214 379
            $this->originalDocumentData        =
2215 379
            $this->documentChangeSets          =
2216 379
            $this->documentStates              =
2217 379
            $this->scheduledForSynchronization =
2218 379
            $this->documentInsertions          =
2219 379
            $this->documentUpserts             =
2220 379
            $this->documentUpdates             =
2221 379
            $this->documentDeletions           =
2222 379
            $this->collectionUpdates           =
2223 379
            $this->collectionDeletions         =
2224 379
            $this->parentAssociations          =
2225 379
            $this->embeddedDocumentsRegistry   =
2226 379
            $this->orphanRemovals              =
2227 379
            $this->hasScheduledCollections     = [];
2228
        } else {
2229 6
            $visited = [];
2230 6
            foreach ($this->identityMap as $className => $documents) {
2231 6
                if ($className !== $documentName) {
2232 3
                    continue;
2233
                }
2234
2235 6
                foreach ($documents as $document) {
2236 6
                    $this->doDetach($document, $visited);
2237
                }
2238
            }
2239
        }
2240
2241 385
        if (! $this->evm->hasListeners(Events::onClear)) {
2242 385
            return;
2243
        }
2244
2245
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2246
    }
2247
2248
    /**
2249
     * Schedules an embedded document for removal. The remove() operation will be
2250
     * invoked on that document at the beginning of the next commit of this
2251
     * UnitOfWork.
2252
     *
2253
     * @internal
2254
     */
2255 58
    public function scheduleOrphanRemoval(object $document) : void
2256
    {
2257 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2258 58
    }
2259
2260
    /**
2261
     * Unschedules an embedded or referenced object for removal.
2262
     *
2263
     * @internal
2264
     */
2265 123
    public function unscheduleOrphanRemoval(object $document) : void
2266
    {
2267 123
        $oid = spl_object_hash($document);
2268 123
        unset($this->orphanRemovals[$oid]);
2269 123
    }
2270
2271
    /**
2272
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2273
     *  1) sets owner if it was cloned
2274
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2275
     *  3) NOP if state is OK
2276
     * Returned collection should be used from now on (only important with 2nd point)
2277
     */
2278 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2279
    {
2280 8
        $owner = $coll->getOwner();
2281 8
        if ($owner === null) { // cloned
2282 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2283 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2284 2
            if (! $coll->isInitialized()) {
2285 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2286
            }
2287 2
            $newValue = clone $coll;
2288 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2289 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2290 2
            if ($this->isScheduledForUpdate($document)) {
2291
                // @todo following line should be superfluous once collections are stored in change sets
2292
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2293
            }
2294
2295 2
            return $newValue;
2296
        }
2297
2298 6
        return $coll;
2299
    }
2300
2301
    /**
2302
     * Schedules a complete collection for removal when this UnitOfWork commits.
2303
     *
2304
     * @internal
2305
     */
2306 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2307
    {
2308 47
        $oid = spl_object_hash($coll);
2309 47
        unset($this->collectionUpdates[$oid]);
2310 47
        if (isset($this->collectionDeletions[$oid])) {
2311
            return;
2312
        }
2313
2314 47
        $this->collectionDeletions[$oid] = $coll;
2315 47
        $this->scheduleCollectionOwner($coll);
2316 47
    }
2317
2318
    /**
2319
     * Checks whether a PersistentCollection is scheduled for deletion.
2320
     *
2321
     * @internal
2322
     */
2323 227
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2324
    {
2325 227
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2326
    }
2327
2328
    /**
2329
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2330
     *
2331
     * @internal
2332
     */
2333 229
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2334
    {
2335 229
        if ($coll->getOwner() === null) {
2336
            return;
2337
        }
2338
2339 229
        $oid = spl_object_hash($coll);
2340 229
        if (! isset($this->collectionDeletions[$oid])) {
2341 229
            return;
2342
        }
2343
2344 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2345 14
        unset($this->collectionDeletions[$oid]);
2346 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2347 14
    }
2348
2349
    /**
2350
     * Schedules a collection for update when this UnitOfWork commits.
2351
     *
2352
     * @internal
2353
     */
2354 256
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2355
    {
2356 256
        $mapping = $coll->getMapping();
2357 256
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2358
            /* There is no need to $unset collection if it will be $set later
2359
             * This is NOP if collection is not scheduled for deletion
2360
             */
2361 44
            $this->unscheduleCollectionDeletion($coll);
2362
        }
2363 256
        $oid = spl_object_hash($coll);
2364 256
        if (isset($this->collectionUpdates[$oid])) {
2365 11
            return;
2366
        }
2367
2368 256
        $this->collectionUpdates[$oid] = $coll;
2369 256
        $this->scheduleCollectionOwner($coll);
2370 256
    }
2371
2372
    /**
2373
     * Unschedules a collection from being updated when this UnitOfWork commits.
2374
     *
2375
     * @internal
2376
     */
2377 229
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2378
    {
2379 229
        if ($coll->getOwner() === null) {
2380
            return;
2381
        }
2382
2383 229
        $oid = spl_object_hash($coll);
2384 229
        if (! isset($this->collectionUpdates[$oid])) {
2385 52
            return;
2386
        }
2387
2388 218
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2389 218
        unset($this->collectionUpdates[$oid]);
2390 218
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2391 218
    }
2392
2393
    /**
2394
     * Checks whether a PersistentCollection is scheduled for update.
2395
     *
2396
     * @internal
2397
     */
2398 141
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2399
    {
2400 141
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2401
    }
2402
2403
    /**
2404
     * Gets PersistentCollections that have been visited during computing change
2405
     * set of $document
2406
     *
2407
     * @internal
2408
     *
2409
     * @return PersistentCollectionInterface[]
2410
     */
2411 607
    public function getVisitedCollections(object $document) : array
2412
    {
2413 607
        $oid = spl_object_hash($document);
2414
2415 607
        return $this->visitedCollections[$oid] ?? [];
2416
    }
2417
2418
    /**
2419
     * Gets PersistentCollections that are scheduled to update and related to $document
2420
     *
2421
     * @internal
2422
     *
2423
     * @return PersistentCollectionInterface[]
2424
     */
2425 607
    public function getScheduledCollections(object $document) : array
2426
    {
2427 607
        $oid = spl_object_hash($document);
2428
2429 607
        return $this->hasScheduledCollections[$oid] ?? [];
2430
    }
2431
2432
    /**
2433
     * Checks whether the document is related to a PersistentCollection
2434
     * scheduled for update or deletion.
2435
     *
2436
     * @internal
2437
     */
2438 57
    public function hasScheduledCollections(object $document) : bool
2439
    {
2440 57
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2441
    }
2442
2443
    /**
2444
     * Marks the PersistentCollection's top-level owner as having a relation to
2445
     * a collection scheduled for update or deletion.
2446
     *
2447
     * If the owner is not scheduled for any lifecycle action, it will be
2448
     * scheduled for update to ensure that versioning takes place if necessary.
2449
     *
2450
     * If the collection is nested within atomic collection, it is immediately
2451
     * unscheduled and atomic one is scheduled for update instead. This makes
2452
     * calculating update data way easier.
2453
     */
2454 258
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2455
    {
2456 258
        if ($coll->getOwner() === null) {
2457
            return;
2458
        }
2459
2460 258
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2461 258
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2462
2463 258
        if ($document !== $coll->getOwner()) {
2464 26
            $parent  = $coll->getOwner();
2465 26
            $mapping = [];
2466 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2467 26
                [$mapping, $parent ] = $parentAssoc;
2468
            }
2469 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2470 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2471 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2472 8
                $this->scheduleCollectionUpdate($atomicCollection);
2473 8
                $this->unscheduleCollectionDeletion($coll);
2474 8
                $this->unscheduleCollectionUpdate($coll);
2475
            }
2476
        }
2477
2478 258
        if ($this->isDocumentScheduled($document)) {
2479 253
            return;
2480
        }
2481
2482 55
        $this->scheduleForUpdate($document);
2483 55
    }
2484
2485
    /**
2486
     * Get the top-most owning document of a given document
2487
     *
2488
     * If a top-level document is provided, that same document will be returned.
2489
     * For an embedded document, we will walk through parent associations until
2490
     * we find a top-level document.
2491
     *
2492
     * @throws UnexpectedValueException When a top-level document could not be found.
2493
     */
2494 260
    public function getOwningDocument(object $document) : object
2495
    {
2496 260
        $class = $this->dm->getClassMetadata(get_class($document));
2497 260
        while ($class->isEmbeddedDocument) {
2498 42
            $parentAssociation = $this->getParentAssociation($document);
2499
2500 42
            if (! $parentAssociation) {
2501
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2502
            }
2503
2504 42
            [, $document ] = $parentAssociation;
2505 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2506
        }
2507
2508 260
        return $document;
2509
    }
2510
2511
    /**
2512
     * Gets the class name for an association (embed or reference) with respect
2513
     * to any discriminator value.
2514
     *
2515
     * @internal
2516
     *
2517
     * @param array|null $data
2518
     */
2519 233
    public function getClassNameForAssociation(array $mapping, $data) : string
2520
    {
2521 233
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2522
2523 233
        $discriminatorValue = null;
2524 233
        if (isset($discriminatorField, $data[$discriminatorField])) {
2525 13
            $discriminatorValue = $data[$discriminatorField];
2526 221
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2527
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2528
        }
2529
2530 233
        if ($discriminatorValue !== null) {
2531 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2532 13
                ?? (string) $discriminatorValue;
2533
        }
2534
2535 221
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2536
2537 221
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2538 11
            $discriminatorValue = $data[$class->discriminatorField];
2539 211
        } elseif ($class->defaultDiscriminatorValue !== null) {
2540 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2541
        }
2542
2543 221
        if ($discriminatorValue !== null) {
2544 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2545
        }
2546
2547 210
        return $mapping['targetDocument'];
2548
    }
2549
2550
    /**
2551
     * Creates a document. Used for reconstitution of documents during hydration.
2552
     */
2553 409
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2554
    {
2555 409
        $class = $this->dm->getClassMetadata($className);
2556
2557
        // @TODO figure out how to remove this
2558 409
        $discriminatorValue = null;
2559 409
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2560 17
            $discriminatorValue = $data[$class->discriminatorField];
2561 399
        } elseif (isset($class->defaultDiscriminatorValue)) {
2562 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2563
        }
2564
2565 409
        if ($discriminatorValue !== null) {
2566 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2567
2568 18
            $class = $this->dm->getClassMetadata($className);
2569
2570 18
            unset($data[$class->discriminatorField]);
2571
        }
2572
2573 409
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2574 2
            $document = $class->newInstance();
2575 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2576
2577 2
            return $document;
2578
        }
2579
2580 408
        $isManagedObject = false;
2581 408
        $serializedId    = null;
2582 408
        $id              = null;
2583 408
        if (! $class->isQueryResultDocument) {
2584 405
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2585 405
            $serializedId    = serialize($id);
2586 405
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2587
        }
2588
2589 408
        $oid = null;
2590 408
        if ($isManagedObject) {
2591 106
            $document = $this->identityMap[$class->name][$serializedId];
2592 106
            $oid      = spl_object_hash($document);
2593 106
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
2594 16
                $document->setProxyInitializer(null);
2595 16
                $overrideLocalValues = true;
2596 16
                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...
2597 16
                    $document->addPropertyChangedListener($this);
2598
                }
2599
            } else {
2600 96
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2601
            }
2602 106
            if ($overrideLocalValues) {
2603 45
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2604 106
                $this->originalDocumentData[$oid] = $data;
2605
            }
2606
        } else {
2607 358
            if ($document === null) {
2608 358
                $document = $class->newInstance();
2609
            }
2610
2611 358
            if (! $class->isQueryResultDocument) {
2612 354
                $this->registerManaged($document, $id, $data);
2613 354
                $oid                                            = spl_object_hash($document);
2614 354
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2615 354
                $this->identityMap[$class->name][$serializedId] = $document;
2616
            }
2617
2618 358
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2619
2620 358
            if (! $class->isQueryResultDocument && ! $class->isView()) {
2621 353
                $this->originalDocumentData[$oid] = $data;
2622
            }
2623
        }
2624
2625 408
        return $document;
2626
    }
2627
2628
    /**
2629
     * Initializes (loads) an uninitialized persistent collection of a document.
2630
     *
2631
     * @internal
2632
     */
2633 181
    public function loadCollection(PersistentCollectionInterface $collection) : void
2634
    {
2635 181
        if ($collection->getOwner() === null) {
2636
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2637
        }
2638
2639 181
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2640 179
        $this->lifecycleEventManager->postCollectionLoad($collection);
2641 179
    }
2642
2643
    /**
2644
     * Gets the identity map of the UnitOfWork.
2645
     *
2646
     * @internal
2647
     */
2648
    public function getIdentityMap() : array
2649
    {
2650
        return $this->identityMap;
2651
    }
2652
2653
    /**
2654
     * Gets the original data of a document. The original data is the data that was
2655
     * present at the time the document was reconstituted from the database.
2656
     *
2657
     * @return array
2658
     */
2659 1
    public function getOriginalDocumentData(object $document) : array
2660
    {
2661 1
        $oid = spl_object_hash($document);
2662
2663 1
        return $this->originalDocumentData[$oid] ?? [];
2664
    }
2665
2666
    /**
2667
     * @internal
2668
     */
2669 61
    public function setOriginalDocumentData(object $document, array $data) : void
2670
    {
2671 61
        $oid                              = spl_object_hash($document);
2672 61
        $this->originalDocumentData[$oid] = $data;
2673 61
        unset($this->documentChangeSets[$oid]);
2674 61
    }
2675
2676
    /**
2677
     * Sets a property value of the original data array of a document.
2678
     *
2679
     * @internal
2680
     *
2681
     * @param mixed $value
2682
     */
2683 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2684
    {
2685 3
        $this->originalDocumentData[$oid][$property] = $value;
2686 3
    }
2687
2688
    /**
2689
     * Gets the identifier of a document.
2690
     *
2691
     * @return mixed The identifier value
2692
     */
2693 484
    public function getDocumentIdentifier(object $document)
2694
    {
2695 484
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2696
    }
2697
2698
    /**
2699
     * Checks whether the UnitOfWork has any pending insertions.
2700
     *
2701
     * @internal
2702
     *
2703
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2704
     */
2705
    public function hasPendingInsertions() : bool
2706
    {
2707
        return ! empty($this->documentInsertions);
2708
    }
2709
2710
    /**
2711
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2712
     * number of documents in the identity map.
2713
     *
2714
     * @internal
2715
     */
2716 2
    public function size() : int
2717
    {
2718 2
        $count = 0;
2719 2
        foreach ($this->identityMap as $documentSet) {
2720 2
            $count += count($documentSet);
2721
        }
2722
2723 2
        return $count;
2724
    }
2725
2726
    /**
2727
     * Registers a document as managed.
2728
     *
2729
     * TODO: This method assumes that $id is a valid PHP identifier for the
2730
     * document class. If the class expects its database identifier to be an
2731
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2732
     * document identifiers map will become inconsistent with the identity map.
2733
     * In the future, we may want to round-trip $id through a PHP and database
2734
     * conversion and throw an exception if it's inconsistent.
2735
     *
2736
     * @internal
2737
     *
2738
     * @param mixed $id The identifier values.
2739
     */
2740 392
    public function registerManaged(object $document, $id, array $data) : void
2741
    {
2742 392
        $oid   = spl_object_hash($document);
2743 392
        $class = $this->dm->getClassMetadata(get_class($document));
2744
2745 392
        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...
2746 110
            $this->documentIdentifiers[$oid] = $oid;
2747
        } else {
2748 386
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2749
        }
2750
2751 392
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2752 392
        $this->originalDocumentData[$oid] = $data;
2753 392
        $this->addToIdentityMap($document);
2754 392
    }
2755
2756
    /**
2757
     * Clears the property changeset of the document with the given OID.
2758
     *
2759
     * @internal
2760
     */
2761
    public function clearDocumentChangeSet(string $oid)
2762
    {
2763
        $this->documentChangeSets[$oid] = [];
2764
    }
2765
2766
    /* PropertyChangedListener implementation */
2767
2768
    /**
2769
     * Notifies this UnitOfWork of a property change in a document.
2770
     *
2771
     * @param object $document     The document that owns the property.
2772
     * @param string $propertyName The name of the property that changed.
2773
     * @param mixed  $oldValue     The old value of the property.
2774
     * @param mixed  $newValue     The new value of the property.
2775
     */
2776 1
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2777
    {
2778 1
        $oid   = spl_object_hash($document);
2779 1
        $class = $this->dm->getClassMetadata(get_class($document));
2780
2781 1
        if (! isset($class->fieldMappings[$propertyName])) {
2782
            return; // ignore non-persistent fields
2783
        }
2784
2785
        // Update changeset and mark document for synchronization
2786 1
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2787 1
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2788
            return;
2789
        }
2790
2791 1
        $this->scheduleForSynchronization($document);
2792 1
    }
2793
2794
    /**
2795
     * Gets the currently scheduled document insertions in this UnitOfWork.
2796
     */
2797 3
    public function getScheduledDocumentInsertions() : array
2798
    {
2799 3
        return $this->documentInsertions;
2800
    }
2801
2802
    /**
2803
     * Gets the currently scheduled document upserts in this UnitOfWork.
2804
     */
2805 1
    public function getScheduledDocumentUpserts() : array
2806
    {
2807 1
        return $this->documentUpserts;
2808
    }
2809
2810
    /**
2811
     * Gets the currently scheduled document updates in this UnitOfWork.
2812
     */
2813 2
    public function getScheduledDocumentUpdates() : array
2814
    {
2815 2
        return $this->documentUpdates;
2816
    }
2817
2818
    /**
2819
     * Gets the currently scheduled document deletions in this UnitOfWork.
2820
     */
2821
    public function getScheduledDocumentDeletions() : array
2822
    {
2823
        return $this->documentDeletions;
2824
    }
2825
2826
    /**
2827
     * Get the currently scheduled complete collection deletions
2828
     *
2829
     * @internal
2830
     */
2831
    public function getScheduledCollectionDeletions() : array
2832
    {
2833
        return $this->collectionDeletions;
2834
    }
2835
2836
    /**
2837
     * Gets the currently scheduled collection inserts, updates and deletes.
2838
     *
2839
     * @internal
2840
     */
2841
    public function getScheduledCollectionUpdates() : array
2842
    {
2843
        return $this->collectionUpdates;
2844
    }
2845
2846
    /**
2847
     * Helper method to initialize a lazy loading proxy or persistent collection.
2848
     *
2849
     * @internal
2850
     */
2851
    public function initializeObject(object $obj) : void
2852
    {
2853
        if ($obj instanceof GhostObjectInterface) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface 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...
2854
            $obj->initializeProxy();
2855
        } elseif ($obj instanceof PersistentCollectionInterface) {
2856
            $obj->initialize();
2857
        }
2858
    }
2859
2860
    private function objToStr(object $obj) : string
2861
    {
2862
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2863
    }
2864
}
2865