Completed
Push — master ( 95608f...d78a07 )
by Andreas
14s queued 10s
created

UnitOfWork::computeScheduleInsertsChangeSets()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 9.9
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
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 1773
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269 1773
        $this->dm                    = $dm;
270 1773
        $this->evm                   = $evm;
271 1773
        $this->hydratorFactory       = $hydratorFactory;
272 1773
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1773
    }
274
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     *
279
     * @internal
280
     */
281 1212
    public function getPersistenceBuilder() : PersistenceBuilder
282
    {
283 1212
        if (! $this->persistenceBuilder) {
284 1212
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
285
        }
286
287 1212
        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 233
    public function getParentAssociation(object $document) : ?array
310
    {
311 233
        $oid = spl_object_hash($document);
312
313 233
        return $this->parentAssociations[$oid] ?? null;
314
    }
315
316
    /**
317
     * Get the document persister instance for the given document name
318
     */
319 1202
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
320
    {
321 1202
        if (! isset($this->persisters[$documentName])) {
322 1202
            $class                           = $this->dm->getClassMetadata($documentName);
323 1202
            $pb                              = $this->getPersistenceBuilder();
324 1202
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
325
        }
326
327 1202
        return $this->persisters[$documentName];
328
    }
329
330
    /**
331
     * Get the collection persister instance.
332
     */
333 1202
    public function getCollectionPersister() : CollectionPersister
334
    {
335 1202
        if (! isset($this->collectionPersister)) {
336 1202
            $pb                        = $this->getPersistenceBuilder();
337 1202
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
338
        }
339
340 1202
        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 613
    public function commit(array $options = []) : void
367
    {
368
        // Raise preFlush
369 613
        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 613
        $this->computeChangeSets();
375
376 612
        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 256
            $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 214
            $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 197
            $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 612
            $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 609
        $this->commitsInProgress++;
388 609
        if ($this->commitsInProgress > 1) {
389
            throw MongoDBException::commitInProgress();
390
        }
391
        try {
392 609
            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 609
            if ($this->evm->hasListeners(Events::onFlush)) {
400 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
401
            }
402
403 608
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
404 85
                [$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 85
                $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 608
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
409 533
                [$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 533
                $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 595
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
414 235
                [$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 235
                $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 595
            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 595
            if ($this->evm->hasListeners(Events::postFlush)) {
425
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
426
            }
427
428
            // Clear up
429 595
            $this->documentInsertions          =
430 595
            $this->documentUpserts             =
431 595
            $this->documentUpdates             =
432 595
            $this->documentDeletions           =
433 595
            $this->documentChangeSets          =
434 595
            $this->collectionUpdates           =
435 595
            $this->collectionDeletions         =
436 595
            $this->visitedCollections          =
437 595
            $this->scheduledForSynchronization =
438 595
            $this->orphanRemovals              =
439 595
            $this->hasScheduledCollections     = [];
440 595
        } finally {
441 609
            $this->commitsInProgress--;
442
        }
443 595
    }
444
445
    /**
446
     * Groups a list of scheduled documents by their class.
447
     */
448 608
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
449
    {
450 608
        if (empty($documents)) {
451 608
            return [];
452
        }
453 607
        $divided = [];
454 607
        $embeds  = [];
455 607
        foreach ($documents as $oid => $d) {
456 607
            $className = get_class($d);
457 607
            if (isset($embeds[$className])) {
458 81
                continue;
459
            }
460 607
            if (isset($divided[$className])) {
461 166
                $divided[$className][1][$oid] = $d;
462 166
                continue;
463
            }
464 607
            $class = $this->dm->getClassMetadata($className);
465 607
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
466 183
                $embeds[$className] = true;
467 183
                continue;
468
            }
469 607
            if (empty($divided[$class->name])) {
470 607
                $divided[$class->name] = [$class, [$oid => $d]];
471
            } else {
472 4
                $divided[$class->name][1][$oid] = $d;
473
            }
474
        }
475
476 607
        return $divided;
477
    }
478
479
    /**
480
     * Compute changesets of all documents scheduled for insertion.
481
     *
482
     * Embedded documents will not be processed.
483
     */
484 618
    private function computeScheduleInsertsChangeSets() : void
485
    {
486 618
        foreach ($this->documentInsertions as $document) {
487 546
            $class = $this->dm->getClassMetadata(get_class($document));
488 546
            if ($class->isEmbeddedDocument) {
489 165
                continue;
490
            }
491
492 540
            $this->computeChangeSet($class, $document);
493
        }
494 617
    }
495
496
    /**
497
     * Compute changesets of all documents scheduled for upsert.
498
     *
499
     * Embedded documents will not be processed.
500
     */
501 617
    private function computeScheduleUpsertsChangeSets() : void
502
    {
503 617
        foreach ($this->documentUpserts as $document) {
504 84
            $class = $this->dm->getClassMetadata(get_class($document));
505 84
            if ($class->isEmbeddedDocument) {
506
                continue;
507
            }
508
509 84
            $this->computeChangeSet($class, $document);
510
        }
511 617
    }
512
513
    /**
514
     * Gets the changeset for a document.
515
     *
516
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
517
     */
518 623
    public function getDocumentChangeSet(object $document) : array
519
    {
520 623
        $oid = spl_object_hash($document);
521
522 623
        return $this->documentChangeSets[$oid] ?? [];
523
    }
524
525
    /**
526
     * Sets the changeset for a document.
527
     *
528
     * @internal
529
     */
530 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
531
    {
532 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
533 1
    }
534
535
    /**
536
     * Get a documents actual data, flattening all the objects to arrays.
537
     *
538
     * @internal
539
     *
540
     * @return array
541
     */
542 626
    public function getDocumentActualData(object $document) : array
543
    {
544 626
        $class      = $this->dm->getClassMetadata(get_class($document));
545 626
        $actualData = [];
546 626
        foreach ($class->reflFields as $name => $refProp) {
547 626
            $mapping = $class->fieldMappings[$name];
548
            // skip not saved fields
549 626
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
550 199
                continue;
551
            }
552 626
            $value = $refProp->getValue($document);
553 626
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
554 626
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
555
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
556 401
                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...
557 147
                    $value = new ArrayCollection($value);
558
                }
559
560
                // Inject PersistentCollection
561 401
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
562 401
                $coll->setOwner($document, $mapping);
563 401
                $coll->setDirty(! $value->isEmpty());
564 401
                $class->reflFields[$name]->setValue($document, $coll);
565 401
                $actualData[$name] = $coll;
566
            } else {
567 626
                $actualData[$name] = $value;
568
            }
569
        }
570
571 626
        return $actualData;
572
    }
573
574
    /**
575
     * Computes the changes that happened to a single document.
576
     *
577
     * Modifies/populates the following properties:
578
     *
579
     * {@link originalDocumentData}
580
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
581
     * then it was not fetched from the database and therefore we have no original
582
     * document data yet. All of the current document data is stored as the original document data.
583
     *
584
     * {@link documentChangeSets}
585
     * The changes detected on all properties of the document are stored there.
586
     * A change is a tuple array where the first entry is the old value and the second
587
     * entry is the new value of the property. Changesets are used by persisters
588
     * to INSERT/UPDATE the persistent document state.
589
     *
590
     * {@link documentUpdates}
591
     * If the document is already fully MANAGED (has been fetched from the database before)
592
     * and any changes to its properties are detected, then a reference to the document is stored
593
     * there to mark it for an update.
594
     */
595 622
    public function computeChangeSet(ClassMetadata $class, object $document) : void
596
    {
597 622
        if (! $class->isInheritanceTypeNone()) {
598 190
            $class = $this->dm->getClassMetadata(get_class($document));
599
        }
600
601
        // Fire PreFlush lifecycle callbacks
602 622
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
603 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
604
        }
605
606 622
        $this->computeOrRecomputeChangeSet($class, $document);
607 621
    }
608
609
    /**
610
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
611
     */
612 622
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
613
    {
614 622
        $oid           = spl_object_hash($document);
615 622
        $actualData    = $this->getDocumentActualData($document);
616 622
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
617 622
        if ($isNewDocument) {
618
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
619
            // These result in an INSERT.
620 621
            $this->originalDocumentData[$oid] = $actualData;
621 621
            $changeSet                        = [];
622 621
            foreach ($actualData as $propName => $actualValue) {
623
                /* At this PersistentCollection shouldn't be here, probably it
624
                 * was cloned and its ownership must be fixed
625
                 */
626 621
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
627
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
628
                    $actualValue           = $actualData[$propName];
629
                }
630
                // ignore inverse side of reference relationship
631 621
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
632 189
                    continue;
633
                }
634 621
                $changeSet[$propName] = [null, $actualValue];
635
            }
636 621
            $this->documentChangeSets[$oid] = $changeSet;
637
        } else {
638 296
            if ($class->isReadOnly) {
639 2
                return;
640
            }
641
            // Document is "fully" MANAGED: it was already fully persisted before
642
            // and we have a copy of the original data
643 294
            $originalData           = $this->originalDocumentData[$oid];
644 294
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
645 294
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
646 1
                $changeSet = $this->documentChangeSets[$oid];
647
            } else {
648 294
                $changeSet = [];
649
            }
650
651 294
            $gridFSMetadataProperty = null;
652
653 294
            if ($class->isFile) {
654
                try {
655 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
656 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
657 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...
658
                }
659
            }
660
661 294
            foreach ($actualData as $propName => $actualValue) {
662
                // skip not saved fields
663 294
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
664 294
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
665 4
                    continue;
666
                }
667
668 293
                $orgValue = $originalData[$propName] ?? null;
669
670
                // skip if value has not changed
671 293
                if ($orgValue === $actualValue) {
672 291
                    if (! $actualValue instanceof PersistentCollectionInterface) {
673 291
                        continue;
674
                    }
675
676 205
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
677
                        // consider dirty collections as changed as well
678 182
                        continue;
679
                    }
680
                }
681
682
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
683 253
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
684 14
                    if ($orgValue !== null) {
685 8
                        $this->scheduleOrphanRemoval($orgValue);
686
                    }
687 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
688 14
                    continue;
689
                }
690
691
                // if owning side of reference-one relationship
692 246
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
693 12
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
694 1
                        $this->scheduleOrphanRemoval($orgValue);
695
                    }
696
697 12
                    $changeSet[$propName] = [$orgValue, $actualValue];
698 12
                    continue;
699
                }
700
701 239
                if ($isChangeTrackingNotify) {
702 2
                    continue;
703
                }
704
705
                // ignore inverse side of reference relationship
706 238
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
707 6
                    continue;
708
                }
709
710
                // Persistent collection was exchanged with the "originally"
711
                // created one. This can only mean it was cloned and replaced
712
                // on another document.
713 236
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
714 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
715
                }
716
717
                // if embed-many or reference-many relationship
718 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
719 125
                    $changeSet[$propName] = [$orgValue, $actualValue];
720
                    /* If original collection was exchanged with a non-empty value
721
                     * and $set will be issued, there is no need to $unset it first
722
                     */
723 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
724 31
                        continue;
725
                    }
726 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
727 18
                        $this->scheduleCollectionDeletion($orgValue);
728
                    }
729 104
                    continue;
730
                }
731
732
                // skip equivalent date values
733 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
734
                    /** @var DateType $dateType */
735 37
                    $dateType      = Type::getType('date');
736 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
737 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
738
739 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...
740 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...
741
742 37
                    if ($orgTimestamp === $actualTimestamp) {
743 30
                        continue;
744
                    }
745
                }
746
747
                // regular field
748 131
                $changeSet[$propName] = [$orgValue, $actualValue];
749
            }
750 294
            if ($changeSet) {
751 242
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
752 18
                    ? $changeSet + $this->documentChangeSets[$oid]
753 239
                    : $changeSet;
754
755 242
                $this->originalDocumentData[$oid] = $actualData;
756 242
                $this->scheduleForUpdate($document);
757
            }
758
        }
759
760
        // Look for changes in associations of the document
761 622
        $associationMappings = array_filter(
762 622
            $class->associationMappings,
763
            static function ($assoc) {
764 479
                return empty($assoc['notSaved']);
765 622
            }
766
        );
767
768 622
        foreach ($associationMappings as $mapping) {
769 479
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
770
771 479
            if ($value === null) {
772 324
                continue;
773
            }
774
775 461
            $this->computeAssociationChanges($document, $mapping, $value);
776
777 460
            if (isset($mapping['reference'])) {
778 346
                continue;
779
            }
780
781 356
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
782
783 356
            foreach ($values as $obj) {
784 188
                $oid2 = spl_object_hash($obj);
785
786 188
                if (isset($this->documentChangeSets[$oid2])) {
787 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
788
                        // instance of $value is the same as it was previously otherwise there would be
789
                        // change set already in place
790 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
791
                    }
792
793 186
                    if (! $isNewDocument) {
794 87
                        $this->scheduleForUpdate($document);
795
                    }
796
797 186
                    break;
798
                }
799
            }
800
        }
801 621
    }
802
803
    /**
804
     * Computes all the changes that have been done to documents and collections
805
     * since the last commit and stores these changes in the _documentChangeSet map
806
     * temporarily for access by the persisters, until the UoW commit is finished.
807
     */
808 618
    public function computeChangeSets() : void
809
    {
810 618
        $this->computeScheduleInsertsChangeSets();
811 617
        $this->computeScheduleUpsertsChangeSets();
812
813
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
814 617
        foreach ($this->identityMap as $className => $documents) {
815 617
            $class = $this->dm->getClassMetadata($className);
816 617
            if ($class->isEmbeddedDocument) {
817
                /* we do not want to compute changes to embedded documents up front
818
                 * in case embedded document was replaced and its changeset
819
                 * would corrupt data. Embedded documents' change set will
820
                 * be calculated by reachability from owning document.
821
                 */
822 178
                continue;
823
            }
824
825
            // If change tracking is explicit or happens through notification, then only compute
826
            // changes on document of that type that are explicitly marked for synchronization.
827
            switch (true) {
828 617
                case $class->isChangeTrackingDeferredImplicit():
829 616
                    $documentsToProcess = $documents;
830 616
                    break;
831
832 3
                case isset($this->scheduledForSynchronization[$className]):
833 2
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
834 2
                    break;
835
836
                default:
837 3
                    $documentsToProcess = [];
838
            }
839
840 617
            foreach ($documentsToProcess as $document) {
841
                // Ignore uninitialized proxy objects
842 612
                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...
843 9
                    continue;
844
                }
845
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
846 612
                $oid = spl_object_hash($document);
847 612
                if (isset($this->documentInsertions[$oid])
848 337
                    || isset($this->documentUpserts[$oid])
849 291
                    || isset($this->documentDeletions[$oid])
850 612
                    || ! isset($this->documentStates[$oid])
851
                ) {
852 610
                    continue;
853
                }
854
855 291
                $this->computeChangeSet($class, $document);
856
            }
857
        }
858 617
    }
859
860
    /**
861
     * Computes the changes of an association.
862
     *
863
     * @param mixed $value The value of the association.
864
     *
865
     * @throws InvalidArgumentException
866
     */
867 461
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
868
    {
869 461
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
870 461
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
871 461
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
872
873 461
        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...
874 7
            return;
875
        }
876
877 460
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
878 257
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
879 253
                $this->scheduleCollectionUpdate($value);
880
            }
881
882 257
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
883 257
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
884 257
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
885 151
                $value->initialize();
886 151
                foreach ($value->getDeletedDocuments() as $orphan) {
887 25
                    $this->scheduleOrphanRemoval($orphan);
888
                }
889
            }
890
        }
891
892
        // Look through the documents, and in any of their associations,
893
        // for transient (new) documents, recursively. ("Persistence by reachability")
894
        // Unwrap. Uninitialized collections will simply be empty.
895 460
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
896
897 460
        $count = 0;
898 460
        foreach ($unwrappedValue as $key => $entry) {
899 374
            if (! is_object($entry)) {
900 1
                throw new InvalidArgumentException(
901 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
902
                );
903
            }
904
905 373
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
906
907 373
            $state = $this->getDocumentState($entry, self::STATE_NEW);
908
909
            // Handle "set" strategy for multi-level hierarchy
910 373
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
911 373
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
912
913 373
            $count++;
914
915
            switch ($state) {
916 373
                case self::STATE_NEW:
917 70
                    if (! $assoc['isCascadePersist']) {
918
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
919
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
920
                            . ' Explicitly persist the new document or configure cascading persist operations'
921
                            . ' on the relationship.');
922
                    }
923
924 70
                    $this->persistNew($targetClass, $entry);
925 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
926 70
                    $this->computeChangeSet($targetClass, $entry);
927 70
                    break;
928
929 369
                case self::STATE_MANAGED:
930 369
                    if ($targetClass->isEmbeddedDocument) {
931 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...
932 179
                        if ($knownParent && $knownParent !== $parentDocument) {
933 6
                            $entry = clone $entry;
934 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
935 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
936 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
937 3
                                $poid = spl_object_hash($parentDocument);
938 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
939 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
940
                                }
941
                            } else {
942
                                // must use unwrapped value to not trigger orphan removal
943 4
                                $unwrappedValue[$key] = $entry;
944
                            }
945 6
                            $this->persistNew($targetClass, $entry);
946
                        }
947 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
948 179
                        $this->computeChangeSet($targetClass, $entry);
949
                    }
950 369
                    break;
951
952 1
                case self::STATE_REMOVED:
953
                    // Consume the $value as array (it's either an array or an ArrayAccess)
954
                    // and remove the element from Collection.
955 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
956
                        unset($value[$key]);
957
                    }
958 1
                    break;
959
960
                case self::STATE_DETACHED:
961
                    // Can actually not happen right now as we assume STATE_NEW,
962
                    // so the exception will be raised from the DBAL layer (constraint violation).
963
                    throw new InvalidArgumentException('A detached document was found through a '
964
                        . 'relationship during cascading a persist operation.');
965
966
                default:
967
                    // MANAGED associated documents are already taken into account
968
                    // during changeset calculation anyway, since they are in the identity map.
969
            }
970
        }
971 459
    }
972
973
    /**
974
     * Computes the changeset of an individual document, independently of the
975
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
976
     *
977
     * The passed document must be a managed document. If the document already has a change set
978
     * because this method is invoked during a commit cycle then the change sets are added.
979
     * whereby changes detected in this method prevail.
980
     *
981
     * @throws InvalidArgumentException If the passed document is not MANAGED.
982
     */
983 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
984
    {
985
        // Ignore uninitialized proxy objects
986 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...
987 1
            return;
988
        }
989
990 18
        $oid = spl_object_hash($document);
991
992 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
993
            throw new InvalidArgumentException('Document must be managed.');
994
        }
995
996 18
        if (! $class->isInheritanceTypeNone()) {
997 2
            $class = $this->dm->getClassMetadata(get_class($document));
998
        }
999
1000 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1001 18
    }
1002
1003
    /**
1004
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1005
     */
1006 654
    private function persistNew(ClassMetadata $class, object $document) : void
1007
    {
1008 654
        $this->lifecycleEventManager->prePersist($class, $document);
1009 654
        $oid    = spl_object_hash($document);
1010 654
        $upsert = false;
1011 654
        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...
1012 654
            $idValue = $class->getIdentifierValue($document);
1013 654
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1014
1015 654
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1016 3
                throw new InvalidArgumentException(sprintf(
1017 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1018 3
                    get_class($document)
1019
                ));
1020
            }
1021
1022 653
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1023 1
                throw new InvalidArgumentException(sprintf(
1024 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1025 1
                    get_class($document)
1026
                ));
1027
            }
1028
1029 652
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1030 571
                $idValue = $class->idGenerator->generate($this->dm, $document);
1031 571
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1032 571
                $class->setIdentifierValue($document, $idValue);
1033
            }
1034
1035 652
            $this->documentIdentifiers[$oid] = $idValue;
1036
        } else {
1037
            // this is for embedded documents without identifiers
1038 152
            $this->documentIdentifiers[$oid] = $oid;
1039
        }
1040
1041 652
        $this->documentStates[$oid] = self::STATE_MANAGED;
1042
1043 652
        if ($upsert) {
1044 91
            $this->scheduleForUpsert($class, $document);
1045
        } else {
1046 580
            $this->scheduleForInsert($class, $document);
1047
        }
1048 652
    }
1049
1050
    /**
1051
     * Executes all document insertions for documents of the specified type.
1052
     */
1053 533
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1054
    {
1055 533
        $persister = $this->getDocumentPersister($class->name);
1056
1057 533
        foreach ($documents as $oid => $document) {
1058 533
            $persister->addInsert($document);
1059 533
            unset($this->documentInsertions[$oid]);
1060
        }
1061
1062 533
        $persister->executeInserts($options);
1063
1064 522
        foreach ($documents as $document) {
1065 522
            $this->lifecycleEventManager->postPersist($class, $document);
1066
        }
1067 522
    }
1068
1069
    /**
1070
     * Executes all document upserts for documents of the specified type.
1071
     */
1072 85
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1073
    {
1074 85
        $persister = $this->getDocumentPersister($class->name);
1075
1076 85
        foreach ($documents as $oid => $document) {
1077 85
            $persister->addUpsert($document);
1078 85
            unset($this->documentUpserts[$oid]);
1079
        }
1080
1081 85
        $persister->executeUpserts($options);
1082
1083 85
        foreach ($documents as $document) {
1084 85
            $this->lifecycleEventManager->postPersist($class, $document);
1085
        }
1086 85
    }
1087
1088
    /**
1089
     * Executes all document updates for documents of the specified type.
1090
     */
1091 235
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1092
    {
1093 235
        if ($class->isReadOnly) {
1094
            return;
1095
        }
1096
1097 235
        $className = $class->name;
1098 235
        $persister = $this->getDocumentPersister($className);
1099
1100 235
        foreach ($documents as $oid => $document) {
1101 235
            $this->lifecycleEventManager->preUpdate($class, $document);
1102
1103 235
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1104 234
                $persister->update($document, $options);
1105
            }
1106
1107 228
            unset($this->documentUpdates[$oid]);
1108
1109 228
            $this->lifecycleEventManager->postUpdate($class, $document);
1110
        }
1111 227
    }
1112
1113
    /**
1114
     * Executes all document deletions for documents of the specified type.
1115
     */
1116 79
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1117
    {
1118 79
        $persister = $this->getDocumentPersister($class->name);
1119
1120 79
        foreach ($documents as $oid => $document) {
1121 79
            if (! $class->isEmbeddedDocument) {
1122 36
                $persister->delete($document, $options);
1123
            }
1124
            unset(
1125 77
                $this->documentDeletions[$oid],
1126 77
                $this->documentIdentifiers[$oid],
1127 77
                $this->originalDocumentData[$oid]
1128
            );
1129
1130
            // Clear snapshot information for any referenced PersistentCollection
1131
            // http://www.doctrine-project.org/jira/browse/MODM-95
1132 77
            foreach ($class->associationMappings as $fieldMapping) {
1133 53
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1134 38
                    continue;
1135
                }
1136
1137 33
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1138 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1139 7
                    continue;
1140
                }
1141
1142 29
                $value->clearSnapshot();
1143
            }
1144
1145
            // Document with this $oid after deletion treated as NEW, even if the $oid
1146
            // is obtained by a new document because the old one went out of scope.
1147 77
            $this->documentStates[$oid] = self::STATE_NEW;
1148
1149 77
            $this->lifecycleEventManager->postRemove($class, $document);
1150
        }
1151 77
    }
1152
1153
    /**
1154
     * Schedules a document for insertion into the database.
1155
     * If the document already has an identifier, it will be added to the
1156
     * identity map.
1157
     *
1158
     * @internal
1159
     *
1160
     * @throws InvalidArgumentException
1161
     */
1162 583
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1163
    {
1164 583
        $oid = spl_object_hash($document);
1165
1166 583
        if (isset($this->documentUpdates[$oid])) {
1167
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1168
        }
1169 583
        if (isset($this->documentDeletions[$oid])) {
1170
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1171
        }
1172 583
        if (isset($this->documentInsertions[$oid])) {
1173
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1174
        }
1175
1176 583
        $this->documentInsertions[$oid] = $document;
1177
1178 583
        if (! isset($this->documentIdentifiers[$oid])) {
1179 3
            return;
1180
        }
1181
1182 580
        $this->addToIdentityMap($document);
1183 580
    }
1184
1185
    /**
1186
     * Schedules a document for upsert into the database and adds it to the
1187
     * identity map
1188
     *
1189
     * @internal
1190
     *
1191
     * @throws InvalidArgumentException
1192
     */
1193 94
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1194
    {
1195 94
        $oid = spl_object_hash($document);
1196
1197 94
        if ($class->isEmbeddedDocument) {
1198
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1199
        }
1200 94
        if (isset($this->documentUpdates[$oid])) {
1201
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1202
        }
1203 94
        if (isset($this->documentDeletions[$oid])) {
1204
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1205
        }
1206 94
        if (isset($this->documentUpserts[$oid])) {
1207
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1208
        }
1209
1210 94
        $this->documentUpserts[$oid]     = $document;
1211 94
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1212 94
        $this->addToIdentityMap($document);
1213 94
    }
1214
1215
    /**
1216
     * Checks whether a document is scheduled for insertion.
1217
     */
1218 109
    public function isScheduledForInsert(object $document) : bool
1219
    {
1220 109
        return isset($this->documentInsertions[spl_object_hash($document)]);
1221
    }
1222
1223
    /**
1224
     * Checks whether a document is scheduled for upsert.
1225
     */
1226 5
    public function isScheduledForUpsert(object $document) : bool
1227
    {
1228 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1229
    }
1230
1231
    /**
1232
     * Schedules a document for being updated.
1233
     *
1234
     * @internal
1235
     *
1236
     * @throws InvalidArgumentException
1237
     */
1238 244
    public function scheduleForUpdate(object $document) : void
1239
    {
1240 244
        $oid = spl_object_hash($document);
1241 244
        if (! isset($this->documentIdentifiers[$oid])) {
1242
            throw new InvalidArgumentException('Document has no identity.');
1243
        }
1244
1245 244
        if (isset($this->documentDeletions[$oid])) {
1246
            throw new InvalidArgumentException('Document is removed.');
1247
        }
1248
1249 244
        if (isset($this->documentUpdates[$oid])
1250 244
            || isset($this->documentInsertions[$oid])
1251 244
            || isset($this->documentUpserts[$oid])) {
1252 104
            return;
1253
        }
1254
1255 242
        $this->documentUpdates[$oid] = $document;
1256 242
    }
1257
1258
    /**
1259
     * Checks whether a document is registered as dirty in the unit of work.
1260
     * Note: Is not very useful currently as dirty documents are only registered
1261
     * at commit time.
1262
     */
1263 21
    public function isScheduledForUpdate(object $document) : bool
1264
    {
1265 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1266
    }
1267
1268
    /**
1269
     * Checks whether a document is registered to be checked in the unit of work.
1270
     */
1271
    public function isScheduledForSynchronization(object $document) : bool
1272
    {
1273
        $class = $this->dm->getClassMetadata(get_class($document));
1274
1275
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1276
    }
1277
1278
    /**
1279
     * Schedules a document for deletion.
1280
     *
1281
     * @internal
1282
     */
1283 84
    public function scheduleForDelete(object $document) : void
1284
    {
1285 84
        $oid = spl_object_hash($document);
1286
1287 84
        if (isset($this->documentInsertions[$oid])) {
1288 2
            if ($this->isInIdentityMap($document)) {
1289 2
                $this->removeFromIdentityMap($document);
1290
            }
1291 2
            unset($this->documentInsertions[$oid]);
1292
1293 2
            return; // document has not been persisted yet, so nothing more to do.
1294
        }
1295
1296 83
        if (! $this->isInIdentityMap($document)) {
1297 2
            return; // ignore
1298
        }
1299
1300 82
        $this->removeFromIdentityMap($document);
1301 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1302
1303 82
        if (isset($this->documentUpdates[$oid])) {
1304
            unset($this->documentUpdates[$oid]);
1305
        }
1306 82
        if (isset($this->documentDeletions[$oid])) {
1307
            return;
1308
        }
1309
1310 82
        $this->documentDeletions[$oid] = $document;
1311 82
    }
1312
1313
    /**
1314
     * Checks whether a document is registered as removed/deleted with the unit
1315
     * of work.
1316
     */
1317 5
    public function isScheduledForDelete(object $document) : bool
1318
    {
1319 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1320
    }
1321
1322
    /**
1323
     * Checks whether a document is scheduled for insertion, update or deletion.
1324
     *
1325
     * @internal
1326
     */
1327 256
    public function isDocumentScheduled(object $document) : bool
1328
    {
1329 256
        $oid = spl_object_hash($document);
1330
1331 256
        return isset($this->documentInsertions[$oid]) ||
1332 138
            isset($this->documentUpserts[$oid]) ||
1333 128
            isset($this->documentUpdates[$oid]) ||
1334 256
            isset($this->documentDeletions[$oid]);
1335
    }
1336
1337
    /**
1338
     * Registers a document in the identity map.
1339
     *
1340
     * Note that documents in a hierarchy are registered with the class name of
1341
     * the root document. Identifiers are serialized before being used as array
1342
     * keys to allow differentiation of equal, but not identical, values.
1343
     *
1344
     * @internal
1345
     */
1346 692
    public function addToIdentityMap(object $document) : bool
1347
    {
1348 692
        $class = $this->dm->getClassMetadata(get_class($document));
1349 692
        $id    = $this->getIdForIdentityMap($document);
1350
1351 692
        if (isset($this->identityMap[$class->name][$id])) {
1352 44
            return false;
1353
        }
1354
1355 692
        $this->identityMap[$class->name][$id] = $document;
1356
1357 692
        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...
1358 692
            ( ! $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...
1359 2
            $document->addPropertyChangedListener($this);
1360
        }
1361
1362 692
        return true;
1363
    }
1364
1365
    /**
1366
     * Gets the state of a document with regard to the current unit of work.
1367
     *
1368
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1369
     *                         This parameter can be set to improve performance of document state detection
1370
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1371
     *                         is either known or does not matter for the caller of the method.
1372
     */
1373 657
    public function getDocumentState(object $document, ?int $assume = null) : int
1374
    {
1375 657
        $oid = spl_object_hash($document);
1376
1377 657
        if (isset($this->documentStates[$oid])) {
1378 412
            return $this->documentStates[$oid];
1379
        }
1380
1381 656
        $class = $this->dm->getClassMetadata(get_class($document));
1382
1383 656
        if ($class->isEmbeddedDocument) {
1384 192
            return self::STATE_NEW;
1385
        }
1386
1387 653
        if ($assume !== null) {
1388 652
            return $assume;
1389
        }
1390
1391
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1392
         * known. Note that you cannot remember the NEW or DETACHED state in
1393
         * _documentStates since the UoW does not hold references to such
1394
         * objects and the object hash can be reused. More generally, because
1395
         * the state may "change" between NEW/DETACHED without the UoW being
1396
         * aware of it.
1397
         */
1398 2
        $id = $class->getIdentifierObject($document);
1399
1400 2
        if ($id === null) {
1401 2
            return self::STATE_NEW;
1402
        }
1403
1404
        // Check for a version field, if available, to avoid a DB lookup.
1405 1
        if ($class->isVersioned && $class->versionField !== null) {
1406
            return $class->getFieldValue($document, $class->versionField)
1407
                ? self::STATE_DETACHED
1408
                : self::STATE_NEW;
1409
        }
1410
1411
        // Last try before DB lookup: check the identity map.
1412 1
        if ($this->tryGetById($id, $class)) {
1413
            return self::STATE_DETACHED;
1414
        }
1415
1416
        // DB lookup
1417 1
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1418 1
            return self::STATE_DETACHED;
1419
        }
1420
1421
        return self::STATE_NEW;
1422
    }
1423
1424
    /**
1425
     * Removes a document from the identity map. This effectively detaches the
1426
     * document from the persistence management of Doctrine.
1427
     *
1428
     * @internal
1429
     *
1430
     * @throws InvalidArgumentException
1431
     */
1432 96
    public function removeFromIdentityMap(object $document) : bool
1433
    {
1434 96
        $oid = spl_object_hash($document);
1435
1436
        // Check if id is registered first
1437 96
        if (! isset($this->documentIdentifiers[$oid])) {
1438
            return false;
1439
        }
1440
1441 96
        $class = $this->dm->getClassMetadata(get_class($document));
1442 96
        $id    = $this->getIdForIdentityMap($document);
1443
1444 96
        if (isset($this->identityMap[$class->name][$id])) {
1445 96
            unset($this->identityMap[$class->name][$id]);
1446 96
            $this->documentStates[$oid] = self::STATE_DETACHED;
1447
1448 96
            return true;
1449
        }
1450
1451
        return false;
1452
    }
1453
1454
    /**
1455
     * Gets a document in the identity map by its identifier hash.
1456
     *
1457
     * @internal
1458
     *
1459
     * @param mixed $id Document identifier
1460
     *
1461
     * @throws InvalidArgumentException If the class does not have an identifier.
1462
     */
1463 37
    public function getById($id, ClassMetadata $class) : object
1464
    {
1465 37
        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...
1466
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1467
        }
1468
1469 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1470
1471 37
        return $this->identityMap[$class->name][$serializedId];
1472
    }
1473
1474
    /**
1475
     * Tries to get a document by its identifier hash. If no document is found
1476
     * for the given hash, FALSE is returned.
1477
     *
1478
     * @internal
1479
     *
1480
     * @param mixed $id Document identifier
1481
     *
1482
     * @return mixed The found document or FALSE.
1483
     *
1484
     * @throws InvalidArgumentException If the class does not have an identifier.
1485
     */
1486 307
    public function tryGetById($id, ClassMetadata $class)
1487
    {
1488 307
        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...
1489
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1490
        }
1491
1492 307
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1493
1494 307
        return $this->identityMap[$class->name][$serializedId] ?? false;
1495
    }
1496
1497
    /**
1498
     * Schedules a document for dirty-checking at commit-time.
1499
     *
1500
     * @internal
1501
     */
1502 2
    public function scheduleForSynchronization(object $document) : void
1503
    {
1504 2
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1505 2
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1506 2
    }
1507
1508
    /**
1509
     * Checks whether a document is registered in the identity map.
1510
     *
1511
     * @internal
1512
     */
1513 90
    public function isInIdentityMap(object $document) : bool
1514
    {
1515 90
        $oid = spl_object_hash($document);
1516
1517 90
        if (! isset($this->documentIdentifiers[$oid])) {
1518 6
            return false;
1519
        }
1520
1521 88
        $class = $this->dm->getClassMetadata(get_class($document));
1522 88
        $id    = $this->getIdForIdentityMap($document);
1523
1524 88
        return isset($this->identityMap[$class->name][$id]);
1525
    }
1526
1527 692
    private function getIdForIdentityMap(object $document) : string
1528
    {
1529 692
        $class = $this->dm->getClassMetadata(get_class($document));
1530
1531 692
        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...
1532 158
            $id = spl_object_hash($document);
1533
        } else {
1534 691
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1535 691
            $id = serialize($class->getDatabaseIdentifierValue($id));
1536
        }
1537
1538 692
        return $id;
1539
    }
1540
1541
    /**
1542
     * Checks whether an identifier exists in the identity map.
1543
     *
1544
     * @internal
1545
     */
1546
    public function containsId($id, string $rootClassName) : bool
1547
    {
1548
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1549
    }
1550
1551
    /**
1552
     * Persists a document as part of the current unit of work.
1553
     *
1554
     * @internal
1555
     *
1556
     * @throws MongoDBException If trying to persist MappedSuperclass.
1557
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1558
     */
1559 655
    public function persist(object $document) : void
1560
    {
1561 655
        $class = $this->dm->getClassMetadata(get_class($document));
1562 655
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1563 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1564
        }
1565 654
        $visited = [];
1566 654
        $this->doPersist($document, $visited);
1567 649
    }
1568
1569
    /**
1570
     * Saves a document as part of the current unit of work.
1571
     * This method is internally called during save() cascades as it tracks
1572
     * the already visited documents to prevent infinite recursions.
1573
     *
1574
     * NOTE: This method always considers documents that are not yet known to
1575
     * this UnitOfWork as NEW.
1576
     *
1577
     * @throws InvalidArgumentException
1578
     * @throws MongoDBException
1579
     */
1580 654
    private function doPersist(object $document, array &$visited) : void
1581
    {
1582 654
        $oid = spl_object_hash($document);
1583 654
        if (isset($visited[$oid])) {
1584 25
            return; // Prevent infinite recursion
1585
        }
1586
1587 654
        $visited[$oid] = $document; // Mark visited
1588
1589 654
        $class = $this->dm->getClassMetadata(get_class($document));
1590
1591 654
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1592
        switch ($documentState) {
1593 654
            case self::STATE_MANAGED:
1594
                // Nothing to do, except if policy is "deferred explicit"
1595 57
                if ($class->isChangeTrackingDeferredExplicit()) {
1596
                    $this->scheduleForSynchronization($document);
1597
                }
1598 57
                break;
1599 654
            case self::STATE_NEW:
1600 654
                if ($class->isFile) {
1601 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1602
                }
1603
1604 653
                $this->persistNew($class, $document);
1605 651
                break;
1606
1607 2
            case self::STATE_REMOVED:
1608
                // Document becomes managed again
1609 2
                unset($this->documentDeletions[$oid]);
1610
1611 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1612 2
                break;
1613
1614
            case self::STATE_DETACHED:
1615
                throw new InvalidArgumentException(
1616
                    'Behavior of persist() for a detached document is not yet defined.'
1617
                );
1618
1619
            default:
1620
                throw MongoDBException::invalidDocumentState($documentState);
1621
        }
1622
1623 651
        $this->cascadePersist($document, $visited);
1624 649
    }
1625
1626
    /**
1627
     * Deletes a document as part of the current unit of work.
1628
     *
1629
     * @internal
1630
     */
1631 83
    public function remove(object $document)
1632
    {
1633 83
        $visited = [];
1634 83
        $this->doRemove($document, $visited);
1635 83
    }
1636
1637
    /**
1638
     * Deletes a document as part of the current unit of work.
1639
     *
1640
     * This method is internally called during delete() cascades as it tracks
1641
     * the already visited documents to prevent infinite recursions.
1642
     *
1643
     * @throws MongoDBException
1644
     */
1645 83
    private function doRemove(object $document, array &$visited) : void
1646
    {
1647 83
        $oid = spl_object_hash($document);
1648 83
        if (isset($visited[$oid])) {
1649 1
            return; // Prevent infinite recursion
1650
        }
1651
1652 83
        $visited[$oid] = $document; // mark visited
1653
1654
        /* Cascade first, because scheduleForDelete() removes the entity from
1655
         * the identity map, which can cause problems when a lazy Proxy has to
1656
         * be initialized for the cascade operation.
1657
         */
1658 83
        $this->cascadeRemove($document, $visited);
1659
1660 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1661 83
        $documentState = $this->getDocumentState($document);
1662
        switch ($documentState) {
1663 83
            case self::STATE_NEW:
1664 83
            case self::STATE_REMOVED:
1665
                // nothing to do
1666 1
                break;
1667 83
            case self::STATE_MANAGED:
1668 83
                $this->lifecycleEventManager->preRemove($class, $document);
1669 83
                $this->scheduleForDelete($document);
1670 83
                break;
1671
            case self::STATE_DETACHED:
1672
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1673
            default:
1674
                throw MongoDBException::invalidDocumentState($documentState);
1675
        }
1676 83
    }
1677
1678
    /**
1679
     * Merges the state of the given detached document into this UnitOfWork.
1680
     *
1681
     * @internal
1682
     */
1683 11
    public function merge(object $document) : object
1684
    {
1685 11
        $visited = [];
1686
1687 11
        return $this->doMerge($document, $visited);
1688
    }
1689
1690
    /**
1691
     * Executes a merge operation on a document.
1692
     *
1693
     * @throws InvalidArgumentException If the entity instance is NEW.
1694
     * @throws LockException If the document uses optimistic locking through a
1695
     *                       version attribute and the version check against the
1696
     *                       managed copy fails.
1697
     */
1698 11
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1699
    {
1700 11
        $oid = spl_object_hash($document);
1701
1702 11
        if (isset($visited[$oid])) {
1703 1
            return $visited[$oid]; // Prevent infinite recursion
1704
        }
1705
1706 11
        $visited[$oid] = $document; // mark visited
1707
1708 11
        $class = $this->dm->getClassMetadata(get_class($document));
1709
1710
        /* First we assume DETACHED, although it can still be NEW but we can
1711
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1712
         * an identity, we need to fetch it from the DB anyway in order to
1713
         * merge. MANAGED documents are ignored by the merge operation.
1714
         */
1715 11
        $managedCopy = $document;
1716
1717 11
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1718 11
            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...
1719
                $document->initializeProxy();
1720
            }
1721
1722 11
            $identifier = $class->getIdentifier();
1723
            // We always have one element in the identifier array but it might be null
1724 11
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1725 11
            $managedCopy = null;
1726
1727
            // Try to fetch document from the database
1728 11
            if (! $class->isEmbeddedDocument && $id !== null) {
1729 11
                $managedCopy = $this->dm->find($class->name, $id);
1730
1731
                // Managed copy may be removed in which case we can't merge
1732 11
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1733
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1734
                }
1735
1736 11
                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...
1737
                    $managedCopy->initializeProxy();
1738
                }
1739
            }
1740
1741 11
            if ($managedCopy === null) {
1742
                // Create a new managed instance
1743 4
                $managedCopy = $class->newInstance();
1744 4
                if ($id !== null) {
1745 3
                    $class->setIdentifierValue($managedCopy, $id);
1746
                }
1747 4
                $this->persistNew($class, $managedCopy);
1748
            }
1749
1750 11
            if ($class->isVersioned) {
1751
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1752
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1753
1754
                // Throw exception if versions don't match
1755
                if ($managedCopyVersion !== $documentVersion) {
1756
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1757
                }
1758
            }
1759
1760
            // Merge state of $document into existing (managed) document
1761 11
            foreach ($class->reflClass->getProperties() as $prop) {
1762 11
                $name = $prop->name;
1763 11
                $prop->setAccessible(true);
1764 11
                if (! isset($class->associationMappings[$name])) {
1765 11
                    if (! $class->isIdentifier($name)) {
1766 11
                        $prop->setValue($managedCopy, $prop->getValue($document));
1767
                    }
1768
                } else {
1769 11
                    $assoc2 = $class->associationMappings[$name];
1770
1771 11
                    if ($assoc2['type'] === 'one') {
1772 5
                        $other = $prop->getValue($document);
1773
1774 5
                        if ($other === null) {
1775 2
                            $prop->setValue($managedCopy, null);
1776 4
                        } 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...
1777
                            // Do not merge fields marked lazy that have not been fetched
1778
                            continue;
1779 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1780
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1781
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1782
                                /** @var ClassMetadata $targetClass */
1783
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1784
                                $relatedId   = $targetClass->getIdentifierObject($other);
1785
1786
                                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...
1787
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1788
                                } else {
1789
                                    $other = $this
1790
                                        ->dm
1791
                                        ->getProxyFactory()
1792
                                        ->getProxy($targetClass, $relatedId);
1793
                                    $this->registerManaged($other, $relatedId, []);
1794
                                }
1795
                            }
1796
1797 5
                            $prop->setValue($managedCopy, $other);
1798
                        }
1799
                    } else {
1800 10
                        $mergeCol = $prop->getValue($document);
1801
1802 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1803
                            /* Do not merge fields marked lazy that have not
1804
                             * been fetched. Keep the lazy persistent collection
1805
                             * of the managed copy.
1806
                             */
1807 3
                            continue;
1808
                        }
1809
1810 10
                        $managedCol = $prop->getValue($managedCopy);
1811
1812 10
                        if (! $managedCol) {
1813 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1814 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1815 1
                            $prop->setValue($managedCopy, $managedCol);
1816 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1817
                        }
1818
1819
                        /* Note: do not process association's target documents.
1820
                         * They will be handled during the cascade. Initialize
1821
                         * and, if necessary, clear $managedCol for now.
1822
                         */
1823 10
                        if ($assoc2['isCascadeMerge']) {
1824 10
                            $managedCol->initialize();
1825
1826
                            // If $managedCol differs from the merged collection, clear and set dirty
1827 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1828 3
                                $managedCol->unwrap()->clear();
1829 3
                                $managedCol->setDirty(true);
1830
1831 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1832
                                    $this->scheduleForSynchronization($managedCopy);
1833
                                }
1834
                            }
1835
                        }
1836
                    }
1837
                }
1838
1839 11
                if (! $class->isChangeTrackingNotify()) {
1840 11
                    continue;
1841
                }
1842
1843
                // Just treat all properties as changed, there is no other choice.
1844
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1845
            }
1846
1847 11
            if ($class->isChangeTrackingDeferredExplicit()) {
1848
                $this->scheduleForSynchronization($document);
1849
            }
1850
        }
1851
1852 11
        if ($prevManagedCopy !== null) {
1853 5
            $assocField = $assoc['fieldName'];
1854 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1855
1856 5
            if ($assoc['type'] === 'one') {
1857 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1858
            } else {
1859 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1860
1861 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1862 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1863
                }
1864
            }
1865
        }
1866
1867
        // Mark the managed copy visited as well
1868 11
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1869
1870 11
        $this->cascadeMerge($document, $managedCopy, $visited);
1871
1872 11
        return $managedCopy;
1873
    }
1874
1875
    /**
1876
     * Detaches a document from the persistence management. It's persistence will
1877
     * no longer be managed by Doctrine.
1878
     *
1879
     * @internal
1880
     */
1881 11
    public function detach(object $document) : void
1882
    {
1883 11
        $visited = [];
1884 11
        $this->doDetach($document, $visited);
1885 11
    }
1886
1887
    /**
1888
     * Executes a detach operation on the given document.
1889
     *
1890
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1891
     */
1892 17
    private function doDetach(object $document, array &$visited) : void
1893
    {
1894 17
        $oid = spl_object_hash($document);
1895 17
        if (isset($visited[$oid])) {
1896 3
            return; // Prevent infinite recursion
1897
        }
1898
1899 17
        $visited[$oid] = $document; // mark visited
1900
1901 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1902 17
            case self::STATE_MANAGED:
1903 17
                $this->removeFromIdentityMap($document);
1904
                unset(
1905 17
                    $this->documentInsertions[$oid],
1906 17
                    $this->documentUpdates[$oid],
1907 17
                    $this->documentDeletions[$oid],
1908 17
                    $this->documentIdentifiers[$oid],
1909 17
                    $this->documentStates[$oid],
1910 17
                    $this->originalDocumentData[$oid],
1911 17
                    $this->parentAssociations[$oid],
1912 17
                    $this->documentUpserts[$oid],
1913 17
                    $this->hasScheduledCollections[$oid],
1914 17
                    $this->embeddedDocumentsRegistry[$oid]
1915
                );
1916 17
                break;
1917 3
            case self::STATE_NEW:
1918 3
            case self::STATE_DETACHED:
1919 3
                return;
1920
        }
1921
1922 17
        $this->cascadeDetach($document, $visited);
1923 17
    }
1924
1925
    /**
1926
     * Refreshes the state of the given document from the database, overwriting
1927
     * any local, unpersisted changes.
1928
     *
1929
     * @internal
1930
     *
1931
     * @throws InvalidArgumentException If the document is not MANAGED.
1932
     */
1933 24
    public function refresh(object $document) : void
1934
    {
1935 24
        $visited = [];
1936 24
        $this->doRefresh($document, $visited);
1937 23
    }
1938
1939
    /**
1940
     * Executes a refresh operation on a document.
1941
     *
1942
     * @throws InvalidArgumentException If the document is not MANAGED.
1943
     */
1944 24
    private function doRefresh(object $document, array &$visited) : void
1945
    {
1946 24
        $oid = spl_object_hash($document);
1947 24
        if (isset($visited[$oid])) {
1948
            return; // Prevent infinite recursion
1949
        }
1950
1951 24
        $visited[$oid] = $document; // mark visited
1952
1953 24
        $class = $this->dm->getClassMetadata(get_class($document));
1954
1955 24
        if (! $class->isEmbeddedDocument) {
1956 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1957 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1958
            }
1959
1960 23
            $this->getDocumentPersister($class->name)->refresh($document);
1961
        }
1962
1963 23
        $this->cascadeRefresh($document, $visited);
1964 23
    }
1965
1966
    /**
1967
     * Cascades a refresh operation to associated documents.
1968
     */
1969 23
    private function cascadeRefresh(object $document, array &$visited) : void
1970
    {
1971 23
        $class = $this->dm->getClassMetadata(get_class($document));
1972
1973 23
        $associationMappings = array_filter(
1974 23
            $class->associationMappings,
1975
            static function ($assoc) {
1976 18
                return $assoc['isCascadeRefresh'];
1977 23
            }
1978
        );
1979
1980 23
        foreach ($associationMappings as $mapping) {
1981 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1982 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...
1983 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1984
                    // Unwrap so that foreach() does not initialize
1985 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1986
                }
1987 15
                foreach ($relatedDocuments as $relatedDocument) {
1988
                    $this->doRefresh($relatedDocument, $visited);
1989
                }
1990 10
            } elseif ($relatedDocuments !== null) {
1991 2
                $this->doRefresh($relatedDocuments, $visited);
1992
            }
1993
        }
1994 23
    }
1995
1996
    /**
1997
     * Cascades a detach operation to associated documents.
1998
     */
1999 17
    private function cascadeDetach(object $document, array &$visited) : void
2000
    {
2001 17
        $class = $this->dm->getClassMetadata(get_class($document));
2002 17
        foreach ($class->fieldMappings as $mapping) {
2003 17
            if (! $mapping['isCascadeDetach']) {
2004 17
                continue;
2005
            }
2006 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2007 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...
2008 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2009
                    // Unwrap so that foreach() does not initialize
2010 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2011
                }
2012 11
                foreach ($relatedDocuments as $relatedDocument) {
2013 8
                    $this->doDetach($relatedDocument, $visited);
2014
                }
2015 11
            } elseif ($relatedDocuments !== null) {
2016 8
                $this->doDetach($relatedDocuments, $visited);
2017
            }
2018
        }
2019 17
    }
2020
2021
    /**
2022
     * Cascades a merge operation to associated documents.
2023
     */
2024 11
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2025
    {
2026 11
        $class = $this->dm->getClassMetadata(get_class($document));
2027
2028 11
        $associationMappings = array_filter(
2029 11
            $class->associationMappings,
2030
            static function ($assoc) {
2031 11
                return $assoc['isCascadeMerge'];
2032 11
            }
2033
        );
2034
2035 11
        foreach ($associationMappings as $assoc) {
2036 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2037
2038 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...
2039 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2040
                    // Collections are the same, so there is nothing to do
2041 1
                    continue;
2042
                }
2043
2044 8
                foreach ($relatedDocuments as $relatedDocument) {
2045 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2046
                }
2047 6
            } elseif ($relatedDocuments !== null) {
2048 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2049
            }
2050
        }
2051 11
    }
2052
2053
    /**
2054
     * Cascades the save operation to associated documents.
2055
     */
2056 651
    private function cascadePersist(object $document, array &$visited) : void
2057
    {
2058 651
        $class = $this->dm->getClassMetadata(get_class($document));
2059
2060 651
        $associationMappings = array_filter(
2061 651
            $class->associationMappings,
2062
            static function ($assoc) {
2063 504
                return $assoc['isCascadePersist'];
2064 651
            }
2065
        );
2066
2067 651
        foreach ($associationMappings as $fieldName => $mapping) {
2068 452
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2069
2070 452
            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...
2071 372
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2072 15
                    if ($relatedDocuments->getOwner() !== $document) {
2073 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2074
                    }
2075
                    // Unwrap so that foreach() does not initialize
2076 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2077
                }
2078
2079 372
                $count = 0;
2080 372
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2081 201
                    if (! empty($mapping['embedded'])) {
2082 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...
2083 130
                        if ($knownParent && $knownParent !== $document) {
2084 1
                            $relatedDocument               = clone $relatedDocument;
2085 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2086
                        }
2087 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2088 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2089
                    }
2090 201
                    $this->doPersist($relatedDocument, $visited);
2091
                }
2092 355
            } elseif ($relatedDocuments !== null) {
2093 136
                if (! empty($mapping['embedded'])) {
2094 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2095 69
                    if ($knownParent && $knownParent !== $document) {
2096 3
                        $relatedDocuments = clone $relatedDocuments;
2097 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2098
                    }
2099 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2100
                }
2101 136
                $this->doPersist($relatedDocuments, $visited);
2102
            }
2103
        }
2104 649
    }
2105
2106
    /**
2107
     * Cascades the delete operation to associated documents.
2108
     */
2109 83
    private function cascadeRemove(object $document, array &$visited) : void
2110
    {
2111 83
        $class = $this->dm->getClassMetadata(get_class($document));
2112 83
        foreach ($class->fieldMappings as $mapping) {
2113 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2114 82
                continue;
2115
            }
2116 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...
2117 3
                $document->initializeProxy();
2118
            }
2119
2120 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2121 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...
2122
                // If its a PersistentCollection initialization is intended! No unwrap!
2123 31
                foreach ($relatedDocuments as $relatedDocument) {
2124 15
                    $this->doRemove($relatedDocument, $visited);
2125
                }
2126 27
            } elseif ($relatedDocuments !== null) {
2127 14
                $this->doRemove($relatedDocuments, $visited);
2128
            }
2129
        }
2130 83
    }
2131
2132
    /**
2133
     * Acquire a lock on the given document.
2134
     *
2135
     * @internal
2136
     *
2137
     * @throws LockException
2138
     * @throws InvalidArgumentException
2139
     */
2140 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2141
    {
2142 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2143 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2144
        }
2145
2146 7
        $documentName = get_class($document);
2147 7
        $class        = $this->dm->getClassMetadata($documentName);
2148
2149 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2150 2
            if (! $class->isVersioned) {
2151 1
                throw LockException::notVersioned($documentName);
2152
            }
2153
2154 1
            if ($lockVersion !== null) {
2155 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2156 1
                if ($documentVersion !== $lockVersion) {
2157 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2158
                }
2159
            }
2160 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2161 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2162
        }
2163 5
    }
2164
2165
    /**
2166
     * Releases a lock on the given document.
2167
     *
2168
     * @internal
2169
     *
2170
     * @throws InvalidArgumentException
2171
     */
2172 1
    public function unlock(object $document) : void
2173
    {
2174 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2175
            throw new InvalidArgumentException('Document is not MANAGED.');
2176
        }
2177 1
        $documentName = get_class($document);
2178 1
        $this->getDocumentPersister($documentName)->unlock($document);
2179 1
    }
2180
2181
    /**
2182
     * Clears the UnitOfWork.
2183
     *
2184
     * @internal
2185
     */
2186 381
    public function clear(?string $documentName = null) : void
2187
    {
2188 381
        if ($documentName === null) {
2189 375
            $this->identityMap                 =
2190 375
            $this->documentIdentifiers         =
2191 375
            $this->originalDocumentData        =
2192 375
            $this->documentChangeSets          =
2193 375
            $this->documentStates              =
2194 375
            $this->scheduledForSynchronization =
2195 375
            $this->documentInsertions          =
2196 375
            $this->documentUpserts             =
2197 375
            $this->documentUpdates             =
2198 375
            $this->documentDeletions           =
2199 375
            $this->collectionUpdates           =
2200 375
            $this->collectionDeletions         =
2201 375
            $this->parentAssociations          =
2202 375
            $this->embeddedDocumentsRegistry   =
2203 375
            $this->orphanRemovals              =
2204 375
            $this->hasScheduledCollections     = [];
2205
        } else {
2206 6
            $visited = [];
2207 6
            foreach ($this->identityMap as $className => $documents) {
2208 6
                if ($className !== $documentName) {
2209 3
                    continue;
2210
                }
2211
2212 6
                foreach ($documents as $document) {
2213 6
                    $this->doDetach($document, $visited);
2214
                }
2215
            }
2216
        }
2217
2218 381
        if (! $this->evm->hasListeners(Events::onClear)) {
2219 381
            return;
2220
        }
2221
2222
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2223
    }
2224
2225
    /**
2226
     * Schedules an embedded document for removal. The remove() operation will be
2227
     * invoked on that document at the beginning of the next commit of this
2228
     * UnitOfWork.
2229
     *
2230
     * @internal
2231
     */
2232 58
    public function scheduleOrphanRemoval(object $document) : void
2233
    {
2234 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2235 58
    }
2236
2237
    /**
2238
     * Unschedules an embedded or referenced object for removal.
2239
     *
2240
     * @internal
2241
     */
2242 123
    public function unscheduleOrphanRemoval(object $document) : void
2243
    {
2244 123
        $oid = spl_object_hash($document);
2245 123
        unset($this->orphanRemovals[$oid]);
2246 123
    }
2247
2248
    /**
2249
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2250
     *  1) sets owner if it was cloned
2251
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2252
     *  3) NOP if state is OK
2253
     * Returned collection should be used from now on (only important with 2nd point)
2254
     */
2255 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2256
    {
2257 8
        $owner = $coll->getOwner();
2258 8
        if ($owner === null) { // cloned
2259 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2260 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2261 2
            if (! $coll->isInitialized()) {
2262 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2263
            }
2264 2
            $newValue = clone $coll;
2265 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2266 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2267 2
            if ($this->isScheduledForUpdate($document)) {
2268
                // @todo following line should be superfluous once collections are stored in change sets
2269
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2270
            }
2271
2272 2
            return $newValue;
2273
        }
2274
2275 6
        return $coll;
2276
    }
2277
2278
    /**
2279
     * Schedules a complete collection for removal when this UnitOfWork commits.
2280
     *
2281
     * @internal
2282
     */
2283 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2284
    {
2285 47
        $oid = spl_object_hash($coll);
2286 47
        unset($this->collectionUpdates[$oid]);
2287 47
        if (isset($this->collectionDeletions[$oid])) {
2288
            return;
2289
        }
2290
2291 47
        $this->collectionDeletions[$oid] = $coll;
2292 47
        $this->scheduleCollectionOwner($coll);
2293 47
    }
2294
2295
    /**
2296
     * Checks whether a PersistentCollection is scheduled for deletion.
2297
     *
2298
     * @internal
2299
     */
2300 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2301
    {
2302 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2303
    }
2304
2305
    /**
2306
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2307
     *
2308
     * @internal
2309
     */
2310 227
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2311
    {
2312 227
        if ($coll->getOwner() === null) {
2313
            return;
2314
        }
2315
2316 227
        $oid = spl_object_hash($coll);
2317 227
        if (! isset($this->collectionDeletions[$oid])) {
2318 227
            return;
2319
        }
2320
2321 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2322 14
        unset($this->collectionDeletions[$oid]);
2323 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2324 14
    }
2325
2326
    /**
2327
     * Schedules a collection for update when this UnitOfWork commits.
2328
     *
2329
     * @internal
2330
     */
2331 253
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2332
    {
2333 253
        $mapping = $coll->getMapping();
2334 253
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2335
            /* There is no need to $unset collection if it will be $set later
2336
             * This is NOP if collection is not scheduled for deletion
2337
             */
2338 44
            $this->unscheduleCollectionDeletion($coll);
2339
        }
2340 253
        $oid = spl_object_hash($coll);
2341 253
        if (isset($this->collectionUpdates[$oid])) {
2342 11
            return;
2343
        }
2344
2345 253
        $this->collectionUpdates[$oid] = $coll;
2346 253
        $this->scheduleCollectionOwner($coll);
2347 253
    }
2348
2349
    /**
2350
     * Unschedules a collection from being updated when this UnitOfWork commits.
2351
     *
2352
     * @internal
2353
     */
2354 227
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2355
    {
2356 227
        if ($coll->getOwner() === null) {
2357
            return;
2358
        }
2359
2360 227
        $oid = spl_object_hash($coll);
2361 227
        if (! isset($this->collectionUpdates[$oid])) {
2362 52
            return;
2363
        }
2364
2365 216
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2366 216
        unset($this->collectionUpdates[$oid]);
2367 216
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2368 216
    }
2369
2370
    /**
2371
     * Checks whether a PersistentCollection is scheduled for update.
2372
     *
2373
     * @internal
2374
     */
2375 140
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2376
    {
2377 140
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2378
    }
2379
2380
    /**
2381
     * Gets PersistentCollections that have been visited during computing change
2382
     * set of $document
2383
     *
2384
     * @internal
2385
     *
2386
     * @return PersistentCollectionInterface[]
2387
     */
2388 595
    public function getVisitedCollections(object $document) : array
2389
    {
2390 595
        $oid = spl_object_hash($document);
2391
2392 595
        return $this->visitedCollections[$oid] ?? [];
2393
    }
2394
2395
    /**
2396
     * Gets PersistentCollections that are scheduled to update and related to $document
2397
     *
2398
     * @internal
2399
     *
2400
     * @return PersistentCollectionInterface[]
2401
     */
2402 595
    public function getScheduledCollections(object $document) : array
2403
    {
2404 595
        $oid = spl_object_hash($document);
2405
2406 595
        return $this->hasScheduledCollections[$oid] ?? [];
2407
    }
2408
2409
    /**
2410
     * Checks whether the document is related to a PersistentCollection
2411
     * scheduled for update or deletion.
2412
     *
2413
     * @internal
2414
     */
2415 57
    public function hasScheduledCollections(object $document) : bool
2416
    {
2417 57
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2418
    }
2419
2420
    /**
2421
     * Marks the PersistentCollection's top-level owner as having a relation to
2422
     * a collection scheduled for update or deletion.
2423
     *
2424
     * If the owner is not scheduled for any lifecycle action, it will be
2425
     * scheduled for update to ensure that versioning takes place if necessary.
2426
     *
2427
     * If the collection is nested within atomic collection, it is immediately
2428
     * unscheduled and atomic one is scheduled for update instead. This makes
2429
     * calculating update data way easier.
2430
     */
2431 255
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2432
    {
2433 255
        if ($coll->getOwner() === null) {
2434
            return;
2435
        }
2436
2437 255
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2438 255
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2439
2440 255
        if ($document !== $coll->getOwner()) {
2441 26
            $parent  = $coll->getOwner();
2442 26
            $mapping = [];
2443 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2444 26
                [$mapping, $parent ] = $parentAssoc;
2445
            }
2446 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2447 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2448 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2449 8
                $this->scheduleCollectionUpdate($atomicCollection);
2450 8
                $this->unscheduleCollectionDeletion($coll);
2451 8
                $this->unscheduleCollectionUpdate($coll);
2452
            }
2453
        }
2454
2455 255
        if ($this->isDocumentScheduled($document)) {
2456 250
            return;
2457
        }
2458
2459 55
        $this->scheduleForUpdate($document);
2460 55
    }
2461
2462
    /**
2463
     * Get the top-most owning document of a given document
2464
     *
2465
     * If a top-level document is provided, that same document will be returned.
2466
     * For an embedded document, we will walk through parent associations until
2467
     * we find a top-level document.
2468
     *
2469
     * @throws UnexpectedValueException When a top-level document could not be found.
2470
     */
2471 257
    public function getOwningDocument(object $document) : object
2472
    {
2473 257
        $class = $this->dm->getClassMetadata(get_class($document));
2474 257
        while ($class->isEmbeddedDocument) {
2475 42
            $parentAssociation = $this->getParentAssociation($document);
2476
2477 42
            if (! $parentAssociation) {
2478
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2479
            }
2480
2481 42
            [, $document ] = $parentAssociation;
2482 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2483
        }
2484
2485 257
        return $document;
2486
    }
2487
2488
    /**
2489
     * Gets the class name for an association (embed or reference) with respect
2490
     * to any discriminator value.
2491
     *
2492
     * @internal
2493
     *
2494
     * @param array|null $data
2495
     */
2496 228
    public function getClassNameForAssociation(array $mapping, $data) : string
2497
    {
2498 228
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2499
2500 228
        $discriminatorValue = null;
2501 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2502 13
            $discriminatorValue = $data[$discriminatorField];
2503 216
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2504
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2505
        }
2506
2507 228
        if ($discriminatorValue !== null) {
2508 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2509 13
                ?? (string) $discriminatorValue;
2510
        }
2511
2512 216
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2513
2514 216
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2515 11
            $discriminatorValue = $data[$class->discriminatorField];
2516 206
        } elseif ($class->defaultDiscriminatorValue !== null) {
2517 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2518
        }
2519
2520 216
        if ($discriminatorValue !== null) {
2521 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2522
        }
2523
2524 205
        return $mapping['targetDocument'];
2525
    }
2526
2527
    /**
2528
     * Creates a document. Used for reconstitution of documents during hydration.
2529
     *
2530
     * @internal
2531
     */
2532 405
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2533
    {
2534 405
        $class = $this->dm->getClassMetadata($className);
2535
2536
        // @TODO figure out how to remove this
2537 405
        $discriminatorValue = null;
2538 405
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2539 17
            $discriminatorValue = $data[$class->discriminatorField];
2540 395
        } elseif (isset($class->defaultDiscriminatorValue)) {
2541 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2542
        }
2543
2544 405
        if ($discriminatorValue !== null) {
2545 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2546
2547 18
            $class = $this->dm->getClassMetadata($className);
2548
2549 18
            unset($data[$class->discriminatorField]);
2550
        }
2551
2552 405
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2553 2
            $document = $class->newInstance();
2554 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2555
2556 2
            return $document;
2557
        }
2558
2559 404
        $isManagedObject = false;
2560 404
        $serializedId    = null;
2561 404
        $id              = null;
2562 404
        if (! $class->isQueryResultDocument) {
2563 401
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2564 401
            $serializedId    = serialize($id);
2565 401
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2566
        }
2567
2568 404
        $oid = null;
2569 404
        if ($isManagedObject) {
2570 103
            $document = $this->identityMap[$class->name][$serializedId];
2571 103
            $oid      = spl_object_hash($document);
2572 103
            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...
2573 16
                $document->setProxyInitializer(null);
2574 16
                $overrideLocalValues = true;
2575 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...
2576 16
                    $document->addPropertyChangedListener($this);
2577
                }
2578
            } else {
2579 93
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2580
            }
2581 103
            if ($overrideLocalValues) {
2582 42
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2583 103
                $this->originalDocumentData[$oid] = $data;
2584
            }
2585
        } else {
2586 355
            if ($document === null) {
2587 355
                $document = $class->newInstance();
2588
            }
2589
2590 355
            if (! $class->isQueryResultDocument) {
2591 351
                $this->registerManaged($document, $id, $data);
2592 351
                $oid                                            = spl_object_hash($document);
2593 351
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2594 351
                $this->identityMap[$class->name][$serializedId] = $document;
2595
            }
2596
2597 355
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2598
2599 355
            if (! $class->isQueryResultDocument) {
2600 351
                $this->originalDocumentData[$oid] = $data;
2601
            }
2602
        }
2603
2604 404
        return $document;
2605
    }
2606
2607
    /**
2608
     * Initializes (loads) an uninitialized persistent collection of a document.
2609
     *
2610
     * @internal
2611
     */
2612 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
2613
    {
2614 178
        if ($collection->getOwner() === null) {
2615
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2616
        }
2617
2618 178
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2619 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2620 178
    }
2621
2622
    /**
2623
     * Gets the identity map of the UnitOfWork.
2624
     *
2625
     * @internal
2626
     */
2627
    public function getIdentityMap() : array
2628
    {
2629
        return $this->identityMap;
2630
    }
2631
2632
    /**
2633
     * Gets the original data of a document. The original data is the data that was
2634
     * present at the time the document was reconstituted from the database.
2635
     *
2636
     * @return array
2637
     */
2638 1
    public function getOriginalDocumentData(object $document) : array
2639
    {
2640 1
        $oid = spl_object_hash($document);
2641
2642 1
        return $this->originalDocumentData[$oid] ?? [];
2643
    }
2644
2645
    /**
2646
     * @internal
2647
     */
2648 60
    public function setOriginalDocumentData(object $document, array $data) : void
2649
    {
2650 60
        $oid                              = spl_object_hash($document);
2651 60
        $this->originalDocumentData[$oid] = $data;
2652 60
        unset($this->documentChangeSets[$oid]);
2653 60
    }
2654
2655
    /**
2656
     * Sets a property value of the original data array of a document.
2657
     *
2658
     * @internal
2659
     *
2660
     * @param mixed $value
2661
     */
2662 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2663
    {
2664 3
        $this->originalDocumentData[$oid][$property] = $value;
2665 3
    }
2666
2667
    /**
2668
     * Gets the identifier of a document.
2669
     *
2670
     * @return mixed The identifier value
2671
     */
2672 473
    public function getDocumentIdentifier(object $document)
2673
    {
2674 473
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2675
    }
2676
2677
    /**
2678
     * Checks whether the UnitOfWork has any pending insertions.
2679
     *
2680
     * @internal
2681
     *
2682
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2683
     */
2684
    public function hasPendingInsertions() : bool
2685
    {
2686
        return ! empty($this->documentInsertions);
2687
    }
2688
2689
    /**
2690
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2691
     * number of documents in the identity map.
2692
     *
2693
     * @internal
2694
     */
2695 2
    public function size() : int
2696
    {
2697 2
        $count = 0;
2698 2
        foreach ($this->identityMap as $documentSet) {
2699 2
            $count += count($documentSet);
2700
        }
2701
2702 2
        return $count;
2703
    }
2704
2705
    /**
2706
     * Registers a document as managed.
2707
     *
2708
     * TODO: This method assumes that $id is a valid PHP identifier for the
2709
     * document class. If the class expects its database identifier to be an
2710
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2711
     * document identifiers map will become inconsistent with the identity map.
2712
     * In the future, we may want to round-trip $id through a PHP and database
2713
     * conversion and throw an exception if it's inconsistent.
2714
     *
2715
     * @internal
2716
     *
2717
     * @param mixed $id The identifier values.
2718
     */
2719 384
    public function registerManaged(object $document, $id, array $data) : void
2720
    {
2721 384
        $oid   = spl_object_hash($document);
2722 384
        $class = $this->dm->getClassMetadata(get_class($document));
2723
2724 384
        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...
2725 110
            $this->documentIdentifiers[$oid] = $oid;
2726
        } else {
2727 378
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2728
        }
2729
2730 384
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2731 384
        $this->originalDocumentData[$oid] = $data;
2732 384
        $this->addToIdentityMap($document);
2733 384
    }
2734
2735
    /**
2736
     * Clears the property changeset of the document with the given OID.
2737
     *
2738
     * @internal
2739
     */
2740
    public function clearDocumentChangeSet(string $oid)
2741
    {
2742
        $this->documentChangeSets[$oid] = [];
2743
    }
2744
2745
    /* PropertyChangedListener implementation */
2746
2747
    /**
2748
     * Notifies this UnitOfWork of a property change in a document.
2749
     *
2750
     * @param object $document     The document that owns the property.
2751
     * @param string $propertyName The name of the property that changed.
2752
     * @param mixed  $oldValue     The old value of the property.
2753
     * @param mixed  $newValue     The new value of the property.
2754
     */
2755 1
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2756
    {
2757 1
        $oid   = spl_object_hash($document);
2758 1
        $class = $this->dm->getClassMetadata(get_class($document));
2759
2760 1
        if (! isset($class->fieldMappings[$propertyName])) {
2761
            return; // ignore non-persistent fields
2762
        }
2763
2764
        // Update changeset and mark document for synchronization
2765 1
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2766 1
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2767
            return;
2768
        }
2769
2770 1
        $this->scheduleForSynchronization($document);
2771 1
    }
2772
2773
    /**
2774
     * Gets the currently scheduled document insertions in this UnitOfWork.
2775
     */
2776 3
    public function getScheduledDocumentInsertions() : array
2777
    {
2778 3
        return $this->documentInsertions;
2779
    }
2780
2781
    /**
2782
     * Gets the currently scheduled document upserts in this UnitOfWork.
2783
     */
2784 1
    public function getScheduledDocumentUpserts() : array
2785
    {
2786 1
        return $this->documentUpserts;
2787
    }
2788
2789
    /**
2790
     * Gets the currently scheduled document updates in this UnitOfWork.
2791
     */
2792 2
    public function getScheduledDocumentUpdates() : array
2793
    {
2794 2
        return $this->documentUpdates;
2795
    }
2796
2797
    /**
2798
     * Gets the currently scheduled document deletions in this UnitOfWork.
2799
     */
2800
    public function getScheduledDocumentDeletions() : array
2801
    {
2802
        return $this->documentDeletions;
2803
    }
2804
2805
    /**
2806
     * Get the currently scheduled complete collection deletions
2807
     *
2808
     * @internal
2809
     */
2810
    public function getScheduledCollectionDeletions() : array
2811
    {
2812
        return $this->collectionDeletions;
2813
    }
2814
2815
    /**
2816
     * Gets the currently scheduled collection inserts, updates and deletes.
2817
     *
2818
     * @internal
2819
     */
2820
    public function getScheduledCollectionUpdates() : array
2821
    {
2822
        return $this->collectionUpdates;
2823
    }
2824
2825
    /**
2826
     * Helper method to initialize a lazy loading proxy or persistent collection.
2827
     *
2828
     * @internal
2829
     */
2830
    public function initializeObject(object $obj) : void
2831
    {
2832
        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...
2833
            $obj->initializeProxy();
2834
        } elseif ($obj instanceof PersistentCollectionInterface) {
2835
            $obj->initialize();
2836
        }
2837
    }
2838
2839
    private function objToStr(object $obj) : string
2840
    {
2841
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2842
    }
2843
}
2844