Completed
Push — master ( b90edc...95608f )
by Andreas
14:15 queued 11s
created

UnitOfWork::getVisitedCollections()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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 1081
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269 1081
        $this->dm                    = $dm;
270 1081
        $this->evm                   = $evm;
271 1081
        $this->hydratorFactory       = $hydratorFactory;
272 1081
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1081
    }
274
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     *
279
     * @internal
280
     */
281 572
    public function getPersistenceBuilder() : PersistenceBuilder
282
    {
283 572
        if (! $this->persistenceBuilder) {
284 572
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
285
        }
286
287 572
        return $this->persistenceBuilder;
288
    }
289
290
    /**
291
     * Sets the parent association for a given embedded document.
292
     *
293
     * @internal
294
     */
295 22
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
296
    {
297 22
        $oid                                   = spl_object_hash($document);
298 22
        $this->embeddedDocumentsRegistry[$oid] = $document;
299 22
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
300 22
    }
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 18
    public function getParentAssociation(object $document) : ?array
310
    {
311 18
        $oid = spl_object_hash($document);
312
313 18
        return $this->parentAssociations[$oid] ?? null;
314
    }
315
316
    /**
317
     * Get the document persister instance for the given document name
318
     */
319 562
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
320
    {
321 562
        if (! isset($this->persisters[$documentName])) {
322 562
            $class                           = $this->dm->getClassMetadata($documentName);
323 562
            $pb                              = $this->getPersistenceBuilder();
324 562
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
325
        }
326
327 562
        return $this->persisters[$documentName];
328
    }
329
330
    /**
331
     * Get the collection persister instance.
332
     */
333 562
    public function getCollectionPersister() : CollectionPersister
334
    {
335 562
        if (! isset($this->collectionPersister)) {
336 562
            $pb                        = $this->getPersistenceBuilder();
337 562
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
338
        }
339
340 562
        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 33
    public function commit(array $options = []) : void
367
    {
368
        // Raise preFlush
369 33
        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 33
        $this->computeChangeSets();
375
376 32
        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 5
            $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
            $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
            $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
            $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
            $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 32
            $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
            return; // Nothing to do.
385
        }
386
387 32
        $this->commitsInProgress++;
388 32
        if ($this->commitsInProgress > 1) {
389
            throw MongoDBException::commitInProgress();
390
        }
391
        try {
392 32
            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
                foreach ($this->orphanRemovals as $removal) {
394
                    $this->remove($removal);
395
                }
396
            }
397
398
            // Raise onFlush
399 32
            if ($this->evm->hasListeners(Events::onFlush)) {
400 1
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
401
            }
402
403 31
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
404 9
                [$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 9
                $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 22
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
409 22
                [$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 22
                $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
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
414
                [$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
                $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
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
419
                [$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
                $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
            if ($this->evm->hasListeners(Events::postFlush)) {
425
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
426
            }
427
428
            // Clear up
429
            $this->documentInsertions          =
430
            $this->documentUpserts             =
431
            $this->documentUpdates             =
432
            $this->documentDeletions           =
433
            $this->documentChangeSets          =
434
            $this->collectionUpdates           =
435
            $this->collectionDeletions         =
436
            $this->visitedCollections          =
437
            $this->scheduledForSynchronization =
438
            $this->orphanRemovals              =
439
            $this->hasScheduledCollections     = [];
440
        } finally {
441 32
            $this->commitsInProgress--;
442
        }
443
    }
444
445
    /**
446
     * Groups a list of scheduled documents by their class.
447
     */
448 31
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
449
    {
450 31
        if (empty($documents)) {
451 22
            return [];
452
        }
453 31
        $divided = [];
454 31
        $embeds  = [];
455 31
        foreach ($documents as $oid => $d) {
456 31
            $className = get_class($d);
457 31
            if (isset($embeds[$className])) {
458
                continue;
459
            }
460 31
            if (isset($divided[$className])) {
461 4
                $divided[$className][1][$oid] = $d;
462 4
                continue;
463
            }
464 31
            $class = $this->dm->getClassMetadata($className);
465 31
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
466 8
                $embeds[$className] = true;
467 8
                continue;
468
            }
469 31
            if (empty($divided[$class->name])) {
470 31
                $divided[$class->name] = [$class, [$oid => $d]];
471
            } else {
472
                $divided[$class->name][1][$oid] = $d;
473
            }
474
        }
475
476 31
        return $divided;
477
    }
478
479
    /**
480
     * Compute changesets of all documents scheduled for insertion.
481
     *
482
     * Embedded documents will not be processed.
483
     */
484 38
    private function computeScheduleInsertsChangeSets() : void
485
    {
486 38
        foreach ($this->documentInsertions as $document) {
487 32
            $class = $this->dm->getClassMetadata(get_class($document));
488 32
            if ($class->isEmbeddedDocument) {
489 9
                continue;
490
            }
491
492 31
            $this->computeChangeSet($class, $document);
493
        }
494 37
    }
495
496
    /**
497
     * Compute changesets of all documents scheduled for upsert.
498
     *
499
     * Embedded documents will not be processed.
500
     */
501 37
    private function computeScheduleUpsertsChangeSets() : void
502
    {
503 37
        foreach ($this->documentUpserts as $document) {
504 9
            $class = $this->dm->getClassMetadata(get_class($document));
505 9
            if ($class->isEmbeddedDocument) {
506
                continue;
507
            }
508
509 9
            $this->computeChangeSet($class, $document);
510
        }
511 37
    }
512
513
    /**
514
     * Gets the changeset for a document.
515
     *
516
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
517
     */
518 48
    public function getDocumentChangeSet(object $document) : array
519
    {
520 48
        $oid = spl_object_hash($document);
521
522 48
        return $this->documentChangeSets[$oid] ?? [];
523
    }
524
525
    /**
526
     * Sets the changeset for a document.
527
     *
528
     * @internal
529
     */
530
    public function setDocumentChangeSet(object $document, array $changeset) : void
531
    {
532
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
533
    }
534
535
    /**
536
     * Get a documents actual data, flattening all the objects to arrays.
537
     *
538
     * @internal
539
     *
540
     * @return array
541
     */
542 50
    public function getDocumentActualData(object $document) : array
543
    {
544 50
        $class      = $this->dm->getClassMetadata(get_class($document));
545 50
        $actualData = [];
546 50
        foreach ($class->reflFields as $name => $refProp) {
547 50
            $mapping = $class->fieldMappings[$name];
548
            // skip not saved fields
549 50
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
550 12
                continue;
551
            }
552 50
            $value = $refProp->getValue($document);
553 50
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
554 50
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
555
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
556 25
                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 2
                    $value = new ArrayCollection($value);
558
                }
559
560
                // Inject PersistentCollection
561 25
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
562 25
                $coll->setOwner($document, $mapping);
563 25
                $coll->setDirty(! $value->isEmpty());
564 25
                $class->reflFields[$name]->setValue($document, $coll);
565 25
                $actualData[$name] = $coll;
566
            } else {
567 50
                $actualData[$name] = $value;
568
            }
569
        }
570
571 50
        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 46
    public function computeChangeSet(ClassMetadata $class, object $document) : void
596
    {
597 46
        if (! $class->isInheritanceTypeNone()) {
598 14
            $class = $this->dm->getClassMetadata(get_class($document));
599
        }
600
601
        // Fire PreFlush lifecycle callbacks
602 46
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
603
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
604
        }
605
606 46
        $this->computeOrRecomputeChangeSet($class, $document);
607 45
    }
608
609
    /**
610
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
611
     */
612 46
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
613
    {
614 46
        $oid           = spl_object_hash($document);
615 46
        $actualData    = $this->getDocumentActualData($document);
616 46
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
617 46
        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 45
            $this->originalDocumentData[$oid] = $actualData;
621 45
            $changeSet                        = [];
622 45
            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 45
                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 45
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
632 6
                    continue;
633
                }
634 45
                $changeSet[$propName] = [null, $actualValue];
635
            }
636 45
            $this->documentChangeSets[$oid] = $changeSet;
637
        } else {
638 3
            if ($class->isReadOnly) {
639
                return;
640
            }
641
            // Document is "fully" MANAGED: it was already fully persisted before
642
            // and we have a copy of the original data
643 3
            $originalData           = $this->originalDocumentData[$oid];
644 3
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
645 3
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
646
                $changeSet = $this->documentChangeSets[$oid];
647
            } else {
648 3
                $changeSet = [];
649
            }
650
651 3
            $gridFSMetadataProperty = null;
652
653 3
            if ($class->isFile) {
654
                try {
655 2
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
656 1
                    $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 3
            foreach ($actualData as $propName => $actualValue) {
662
                // skip not saved fields
663 3
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
664 3
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
665 2
                    continue;
666
                }
667
668 2
                $orgValue = $originalData[$propName] ?? null;
669
670
                // skip if value has not changed
671 2
                if ($orgValue === $actualValue) {
672 1
                    if (! $actualValue instanceof PersistentCollectionInterface) {
673 1
                        continue;
674
                    }
675
676 1
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
677
                        // consider dirty collections as changed as well
678 1
                        continue;
679
                    }
680
                }
681
682
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
683 1
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
684 1
                    if ($orgValue !== null) {
685
                        $this->scheduleOrphanRemoval($orgValue);
686
                    }
687 1
                    $changeSet[$propName] = [$orgValue, $actualValue];
688 1
                    continue;
689
                }
690
691
                // if owning side of reference-one relationship
692
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
693
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
694
                        $this->scheduleOrphanRemoval($orgValue);
695
                    }
696
697
                    $changeSet[$propName] = [$orgValue, $actualValue];
698
                    continue;
699
                }
700
701
                if ($isChangeTrackingNotify) {
702
                    continue;
703
                }
704
705
                // ignore inverse side of reference relationship
706
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
707
                    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
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
714
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
715
                }
716
717
                // if embed-many or reference-many relationship
718
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
719
                    $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
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
724
                        continue;
725
                    }
726
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
727
                        $this->scheduleCollectionDeletion($orgValue);
728
                    }
729
                    continue;
730
                }
731
732
                // skip equivalent date values
733
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
734
                    /** @var DateType $dateType */
735
                    $dateType      = Type::getType('date');
736
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
737
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
738
739
                    $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
                    $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
                    if ($orgTimestamp === $actualTimestamp) {
743
                        continue;
744
                    }
745
                }
746
747
                // regular field
748
                $changeSet[$propName] = [$orgValue, $actualValue];
749
            }
750 3
            if ($changeSet) {
751 1
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
752
                    ? $changeSet + $this->documentChangeSets[$oid]
753 1
                    : $changeSet;
754
755 1
                $this->originalDocumentData[$oid] = $actualData;
756 1
                $this->scheduleForUpdate($document);
757
            }
758
        }
759
760
        // Look for changes in associations of the document
761 46
        $associationMappings = array_filter(
762 46
            $class->associationMappings,
763
            static function ($assoc) {
764 39
                return empty($assoc['notSaved']);
765 46
            }
766
        );
767
768 46
        foreach ($associationMappings as $mapping) {
769 39
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
770
771 39
            if ($value === null) {
772 22
                continue;
773
            }
774
775 37
            $this->computeAssociationChanges($document, $mapping, $value);
776
777 36
            if (isset($mapping['reference'])) {
778 30
                continue;
779
            }
780
781 24
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
782
783 24
            foreach ($values as $obj) {
784 12
                $oid2 = spl_object_hash($obj);
785
786 12
                if (isset($this->documentChangeSets[$oid2])) {
787 10
                    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
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
791
                    }
792
793 10
                    if (! $isNewDocument) {
794 2
                        $this->scheduleForUpdate($document);
795
                    }
796
797 10
                    break;
798
                }
799
            }
800
        }
801 45
    }
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 38
    public function computeChangeSets() : void
809
    {
810 38
        $this->computeScheduleInsertsChangeSets();
811 37
        $this->computeScheduleUpsertsChangeSets();
812
813
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
814 37
        foreach ($this->identityMap as $className => $documents) {
815 37
            $class = $this->dm->getClassMetadata($className);
816 37
            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 9
                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 37
                case $class->isChangeTrackingDeferredImplicit():
829 37
                    $documentsToProcess = $documents;
830 37
                    break;
831
832
                case isset($this->scheduledForSynchronization[$className]):
833
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
834
                    break;
835
836
                default:
837
                    $documentsToProcess = [];
838
            }
839
840 37
            foreach ($documentsToProcess as $document) {
841
                // Ignore uninitialized proxy objects
842 37
                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
                    continue;
844
                }
845
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
846 37
                $oid = spl_object_hash($document);
847 37
                if (isset($this->documentInsertions[$oid])
848 11
                    || isset($this->documentUpserts[$oid])
849 2
                    || isset($this->documentDeletions[$oid])
850 37
                    || ! isset($this->documentStates[$oid])
851
                ) {
852 36
                    continue;
853
                }
854
855 2
                $this->computeChangeSet($class, $document);
856
            }
857
        }
858 37
    }
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 37
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
868
    {
869 37
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
870 37
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
871 37
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
872
873 37
        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
            return;
875
        }
876
877 37
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
878 9
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
879 9
                $this->scheduleCollectionUpdate($value);
880
            }
881
882 9
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
883 9
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
884 9
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
885 6
                $value->initialize();
886 6
                foreach ($value->getDeletedDocuments() as $orphan) {
887
                    $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 37
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
896
897 37
        $count = 0;
898 37
        foreach ($unwrappedValue as $key => $entry) {
899 31
            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 30
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
906
907 30
            $state = $this->getDocumentState($entry, self::STATE_NEW);
908
909
            // Handle "set" strategy for multi-level hierarchy
910 30
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
911 30
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
912
913 30
            $count++;
914
915
            switch ($state) {
916 30
                case self::STATE_NEW:
917 1
                    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 1
                    $this->persistNew($targetClass, $entry);
925 1
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
926 1
                    $this->computeChangeSet($targetClass, $entry);
927 1
                    break;
928
929 30
                case self::STATE_MANAGED:
930 30
                    if ($targetClass->isEmbeddedDocument) {
931 9
                        [, $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 9
                        if ($knownParent && $knownParent !== $parentDocument) {
933
                            $entry = clone $entry;
934
                            if ($assoc['type'] === ClassMetadata::ONE) {
935
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
936
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
937
                                $poid = spl_object_hash($parentDocument);
938
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
939
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
940
                                }
941
                            } else {
942
                                // must use unwrapped value to not trigger orphan removal
943
                                $unwrappedValue[$key] = $entry;
944
                            }
945
                            $this->persistNew($targetClass, $entry);
946
                        }
947 9
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
948 9
                        $this->computeChangeSet($targetClass, $entry);
949
                    }
950 30
                    break;
951
952
                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
                    if ($assoc['type'] === ClassMetadata::MANY) {
956
                        unset($value[$key]);
957
                    }
958
                    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 36
    }
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
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
984
    {
985
        // Ignore uninitialized proxy objects
986
        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
            return;
988
        }
989
990
        $oid = spl_object_hash($document);
991
992
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
993
            throw new InvalidArgumentException('Document must be managed.');
994
        }
995
996
        if (! $class->isInheritanceTypeNone()) {
997
            $class = $this->dm->getClassMetadata(get_class($document));
998
        }
999
1000
        $this->computeOrRecomputeChangeSet($class, $document, true);
1001
    }
1002
1003
    /**
1004
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1005
     */
1006 75
    private function persistNew(ClassMetadata $class, object $document) : void
1007
    {
1008 75
        $this->lifecycleEventManager->prePersist($class, $document);
1009 75
        $oid    = spl_object_hash($document);
1010 75
        $upsert = false;
1011 75
        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 75
            $idValue = $class->getIdentifierValue($document);
1013 75
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1014
1015 75
            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 74
            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 73
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1030 60
                $idValue = $class->idGenerator->generate($this->dm, $document);
1031 60
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1032 60
                $class->setIdentifierValue($document, $idValue);
1033
            }
1034
1035 73
            $this->documentIdentifiers[$oid] = $idValue;
1036
        } else {
1037
            // this is for embedded documents without identifiers
1038 11
            $this->documentIdentifiers[$oid] = $oid;
1039
        }
1040
1041 73
        $this->documentStates[$oid] = self::STATE_MANAGED;
1042
1043 73
        if ($upsert) {
1044 14
            $this->scheduleForUpsert($class, $document);
1045
        } else {
1046 63
            $this->scheduleForInsert($class, $document);
1047
        }
1048 73
    }
1049
1050
    /**
1051
     * Executes all document insertions for documents of the specified type.
1052
     */
1053 22
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1054
    {
1055 22
        $persister = $this->getDocumentPersister($class->name);
1056
1057 22
        foreach ($documents as $oid => $document) {
1058 22
            $persister->addInsert($document);
1059 22
            unset($this->documentInsertions[$oid]);
1060
        }
1061
1062 22
        $persister->executeInserts($options);
1063
1064
        foreach ($documents as $document) {
1065
            $this->lifecycleEventManager->postPersist($class, $document);
1066
        }
1067
    }
1068
1069
    /**
1070
     * Executes all document upserts for documents of the specified type.
1071
     */
1072 9
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1073
    {
1074 9
        $persister = $this->getDocumentPersister($class->name);
1075
1076 9
        foreach ($documents as $oid => $document) {
1077 9
            $persister->addUpsert($document);
1078 9
            unset($this->documentUpserts[$oid]);
1079
        }
1080
1081 9
        $persister->executeUpserts($options);
1082
1083
        foreach ($documents as $document) {
1084
            $this->lifecycleEventManager->postPersist($class, $document);
1085
        }
1086
    }
1087
1088
    /**
1089
     * Executes all document updates for documents of the specified type.
1090
     */
1091
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1092
    {
1093
        if ($class->isReadOnly) {
1094
            return;
1095
        }
1096
1097
        $className = $class->name;
1098
        $persister = $this->getDocumentPersister($className);
1099
1100
        foreach ($documents as $oid => $document) {
1101
            $this->lifecycleEventManager->preUpdate($class, $document);
1102
1103
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1104
                $persister->update($document, $options);
1105
            }
1106
1107
            unset($this->documentUpdates[$oid]);
1108
1109
            $this->lifecycleEventManager->postUpdate($class, $document);
1110
        }
1111
    }
1112
1113
    /**
1114
     * Executes all document deletions for documents of the specified type.
1115
     */
1116
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1117
    {
1118
        $persister = $this->getDocumentPersister($class->name);
1119
1120
        foreach ($documents as $oid => $document) {
1121
            if (! $class->isEmbeddedDocument) {
1122
                $persister->delete($document, $options);
1123
            }
1124
            unset(
1125
                $this->documentDeletions[$oid],
1126
                $this->documentIdentifiers[$oid],
1127
                $this->originalDocumentData[$oid]
1128
            );
1129
1130
            // Clear snapshot information for any referenced PersistentCollection
1131
            // http://www.doctrine-project.org/jira/browse/MODM-95
1132
            foreach ($class->associationMappings as $fieldMapping) {
1133
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1134
                    continue;
1135
                }
1136
1137
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1138
                if (! ($value instanceof PersistentCollectionInterface)) {
1139
                    continue;
1140
                }
1141
1142
                $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
            $this->documentStates[$oid] = self::STATE_NEW;
1148
1149
            $this->lifecycleEventManager->postRemove($class, $document);
1150
        }
1151
    }
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 66
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1163
    {
1164 66
        $oid = spl_object_hash($document);
1165
1166 66
        if (isset($this->documentUpdates[$oid])) {
1167
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1168
        }
1169 66
        if (isset($this->documentDeletions[$oid])) {
1170
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1171
        }
1172 66
        if (isset($this->documentInsertions[$oid])) {
1173
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1174
        }
1175
1176 66
        $this->documentInsertions[$oid] = $document;
1177
1178 66
        if (! isset($this->documentIdentifiers[$oid])) {
1179 3
            return;
1180
        }
1181
1182 63
        $this->addToIdentityMap($document);
1183 63
    }
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 17
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1194
    {
1195 17
        $oid = spl_object_hash($document);
1196
1197 17
        if ($class->isEmbeddedDocument) {
1198
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1199
        }
1200 17
        if (isset($this->documentUpdates[$oid])) {
1201
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1202
        }
1203 17
        if (isset($this->documentDeletions[$oid])) {
1204
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1205
        }
1206 17
        if (isset($this->documentUpserts[$oid])) {
1207
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1208
        }
1209
1210 17
        $this->documentUpserts[$oid]     = $document;
1211 17
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1212 17
        $this->addToIdentityMap($document);
1213 17
    }
1214
1215
    /**
1216
     * Checks whether a document is scheduled for insertion.
1217
     */
1218 8
    public function isScheduledForInsert(object $document) : bool
1219
    {
1220 8
        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 2
    public function scheduleForUpdate(object $document) : void
1239
    {
1240 2
        $oid = spl_object_hash($document);
1241 2
        if (! isset($this->documentIdentifiers[$oid])) {
1242
            throw new InvalidArgumentException('Document has no identity.');
1243
        }
1244
1245 2
        if (isset($this->documentDeletions[$oid])) {
1246
            throw new InvalidArgumentException('Document is removed.');
1247
        }
1248
1249 2
        if (isset($this->documentUpdates[$oid])
1250 2
            || isset($this->documentInsertions[$oid])
1251 2
            || isset($this->documentUpserts[$oid])) {
1252 2
            return;
1253
        }
1254
1255 1
        $this->documentUpdates[$oid] = $document;
1256 1
    }
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 4
    public function isScheduledForUpdate(object $document) : bool
1264
    {
1265 4
        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 1
    public function scheduleForDelete(object $document) : void
1284
    {
1285 1
        $oid = spl_object_hash($document);
1286
1287 1
        if (isset($this->documentInsertions[$oid])) {
1288
            if ($this->isInIdentityMap($document)) {
1289
                $this->removeFromIdentityMap($document);
1290
            }
1291
            unset($this->documentInsertions[$oid]);
1292
1293
            return; // document has not been persisted yet, so nothing more to do.
1294
        }
1295
1296 1
        if (! $this->isInIdentityMap($document)) {
1297 1
            return; // ignore
1298
        }
1299
1300
        $this->removeFromIdentityMap($document);
1301
        $this->documentStates[$oid] = self::STATE_REMOVED;
1302
1303
        if (isset($this->documentUpdates[$oid])) {
1304
            unset($this->documentUpdates[$oid]);
1305
        }
1306
        if (isset($this->documentDeletions[$oid])) {
1307
            return;
1308
        }
1309
1310
        $this->documentDeletions[$oid] = $document;
1311
    }
1312
1313
    /**
1314
     * Checks whether a document is registered as removed/deleted with the unit
1315
     * of work.
1316
     */
1317 2
    public function isScheduledForDelete(object $document) : bool
1318
    {
1319 2
        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 10
    public function isDocumentScheduled(object $document) : bool
1328
    {
1329 10
        $oid = spl_object_hash($document);
1330
1331 10
        return isset($this->documentInsertions[$oid]) ||
1332 2
            isset($this->documentUpserts[$oid]) ||
1333 1
            isset($this->documentUpdates[$oid]) ||
1334 10
            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 84
    public function addToIdentityMap(object $document) : bool
1347
    {
1348 84
        $class = $this->dm->getClassMetadata(get_class($document));
1349 84
        $id    = $this->getIdForIdentityMap($document);
1350
1351 84
        if (isset($this->identityMap[$class->name][$id])) {
1352
            return false;
1353
        }
1354
1355 84
        $this->identityMap[$class->name][$id] = $document;
1356
1357 84
        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 84
            ( ! $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
            $document->addPropertyChangedListener($this);
1360
        }
1361
1362 84
        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 78
    public function getDocumentState(object $document, ?int $assume = null) : int
1374
    {
1375 78
        $oid = spl_object_hash($document);
1376
1377 78
        if (isset($this->documentStates[$oid])) {
1378 33
            return $this->documentStates[$oid];
1379
        }
1380
1381 78
        $class = $this->dm->getClassMetadata(get_class($document));
1382
1383 78
        if ($class->isEmbeddedDocument) {
1384 19
            return self::STATE_NEW;
1385
        }
1386
1387 75
        if ($assume !== null) {
1388 74
            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 1
        $id = $class->getIdentifierObject($document);
1399
1400 1
        if ($id === null) {
1401 1
            return self::STATE_NEW;
1402
        }
1403
1404
        // Check for a version field, if available, to avoid a DB lookup.
1405
        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
        if ($this->tryGetById($id, $class)) {
1413
            return self::STATE_DETACHED;
1414
        }
1415
1416
        // DB lookup
1417
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1418
            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 3
    public function removeFromIdentityMap(object $document) : bool
1433
    {
1434 3
        $oid = spl_object_hash($document);
1435
1436
        // Check if id is registered first
1437 3
        if (! isset($this->documentIdentifiers[$oid])) {
1438
            return false;
1439
        }
1440
1441 3
        $class = $this->dm->getClassMetadata(get_class($document));
1442 3
        $id    = $this->getIdForIdentityMap($document);
1443
1444 3
        if (isset($this->identityMap[$class->name][$id])) {
1445 3
            unset($this->identityMap[$class->name][$id]);
1446 3
            $this->documentStates[$oid] = self::STATE_DETACHED;
1447
1448 3
            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
    public function getById($id, ClassMetadata $class) : object
1464
    {
1465
        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
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1470
1471
        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 4
    public function tryGetById($id, ClassMetadata $class)
1487
    {
1488 4
        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 4
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1493
1494 4
        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
    public function scheduleForSynchronization(object $document) : void
1503
    {
1504
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1505
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1506
    }
1507
1508
    /**
1509
     * Checks whether a document is registered in the identity map.
1510
     *
1511
     * @internal
1512
     */
1513 4
    public function isInIdentityMap(object $document) : bool
1514
    {
1515 4
        $oid = spl_object_hash($document);
1516
1517 4
        if (! isset($this->documentIdentifiers[$oid])) {
1518 3
            return false;
1519
        }
1520
1521 2
        $class = $this->dm->getClassMetadata(get_class($document));
1522 2
        $id    = $this->getIdForIdentityMap($document);
1523
1524 2
        return isset($this->identityMap[$class->name][$id]);
1525
    }
1526
1527 84
    private function getIdForIdentityMap(object $document) : string
1528
    {
1529 84
        $class = $this->dm->getClassMetadata(get_class($document));
1530
1531 84
        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 13
            $id = spl_object_hash($document);
1533
        } else {
1534 83
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1535 83
            $id = serialize($class->getDatabaseIdentifierValue($id));
1536
        }
1537
1538 84
        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 77
    public function persist(object $document) : void
1560
    {
1561 77
        $class = $this->dm->getClassMetadata(get_class($document));
1562 77
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1563 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1564
        }
1565 76
        $visited = [];
1566 76
        $this->doPersist($document, $visited);
1567 71
    }
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 76
    private function doPersist(object $document, array &$visited) : void
1581
    {
1582 76
        $oid = spl_object_hash($document);
1583 76
        if (isset($visited[$oid])) {
1584
            return; // Prevent infinite recursion
1585
        }
1586
1587 76
        $visited[$oid] = $document; // Mark visited
1588
1589 76
        $class = $this->dm->getClassMetadata(get_class($document));
1590
1591 76
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1592
        switch ($documentState) {
1593 76
            case self::STATE_MANAGED:
1594
                // Nothing to do, except if policy is "deferred explicit"
1595 1
                if ($class->isChangeTrackingDeferredExplicit()) {
1596
                    $this->scheduleForSynchronization($document);
1597
                }
1598 1
                break;
1599 76
            case self::STATE_NEW:
1600 76
                if ($class->isFile) {
1601 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1602
                }
1603
1604 75
                $this->persistNew($class, $document);
1605 73
                break;
1606
1607
            case self::STATE_REMOVED:
1608
                // Document becomes managed again
1609
                unset($this->documentDeletions[$oid]);
1610
1611
                $this->documentStates[$oid] = self::STATE_MANAGED;
1612
                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 73
        $this->cascadePersist($document, $visited);
1624 71
    }
1625
1626
    /**
1627
     * Deletes a document as part of the current unit of work.
1628
     *
1629
     * @internal
1630
     */
1631
    public function remove(object $document)
1632
    {
1633
        $visited = [];
1634
        $this->doRemove($document, $visited);
1635
    }
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
    private function doRemove(object $document, array &$visited) : void
1646
    {
1647
        $oid = spl_object_hash($document);
1648
        if (isset($visited[$oid])) {
1649
            return; // Prevent infinite recursion
1650
        }
1651
1652
        $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
        $this->cascadeRemove($document, $visited);
1659
1660
        $class         = $this->dm->getClassMetadata(get_class($document));
1661
        $documentState = $this->getDocumentState($document);
1662
        switch ($documentState) {
1663
            case self::STATE_NEW:
1664
            case self::STATE_REMOVED:
1665
                // nothing to do
1666
                break;
1667
            case self::STATE_MANAGED:
1668
                $this->lifecycleEventManager->preRemove($class, $document);
1669
                $this->scheduleForDelete($document);
1670
                break;
1671
            case self::STATE_DETACHED:
1672
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1673
            default:
1674
                throw MongoDBException::invalidDocumentState($documentState);
1675
        }
1676
    }
1677
1678
    /**
1679
     * Merges the state of the given detached document into this UnitOfWork.
1680
     *
1681
     * @internal
1682
     */
1683 1
    public function merge(object $document) : object
1684
    {
1685 1
        $visited = [];
1686
1687 1
        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 1
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1699
    {
1700 1
        $oid = spl_object_hash($document);
1701
1702 1
        if (isset($visited[$oid])) {
1703
            return $visited[$oid]; // Prevent infinite recursion
1704
        }
1705
1706 1
        $visited[$oid] = $document; // mark visited
1707
1708 1
        $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 1
        $managedCopy = $document;
1716
1717 1
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1718 1
            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 1
            $identifier = $class->getIdentifier();
1723
            // We always have one element in the identifier array but it might be null
1724 1
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1725 1
            $managedCopy = null;
1726
1727
            // Try to fetch document from the database
1728 1
            if (! $class->isEmbeddedDocument && $id !== null) {
1729 1
                $managedCopy = $this->dm->find($class->name, $id);
1730
1731
                // Managed copy may be removed in which case we can't merge
1732
                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
                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
            if ($managedCopy === null) {
1742
                // Create a new managed instance
1743
                $managedCopy = $class->newInstance();
1744
                if ($id !== null) {
1745
                    $class->setIdentifierValue($managedCopy, $id);
1746
                }
1747
                $this->persistNew($class, $managedCopy);
1748
            }
1749
1750
            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
            foreach ($class->reflClass->getProperties() as $prop) {
1762
                $name = $prop->name;
1763
                $prop->setAccessible(true);
1764
                if (! isset($class->associationMappings[$name])) {
1765
                    if (! $class->isIdentifier($name)) {
1766
                        $prop->setValue($managedCopy, $prop->getValue($document));
1767
                    }
1768
                } else {
1769
                    $assoc2 = $class->associationMappings[$name];
1770
1771
                    if ($assoc2['type'] === 'one') {
1772
                        $other = $prop->getValue($document);
1773
1774
                        if ($other === null) {
1775
                            $prop->setValue($managedCopy, null);
1776
                        } 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
                        } 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
                            $prop->setValue($managedCopy, $other);
1798
                        }
1799
                    } else {
1800
                        $mergeCol = $prop->getValue($document);
1801
1802
                        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
                            continue;
1808
                        }
1809
1810
                        $managedCol = $prop->getValue($managedCopy);
1811
1812
                        if (! $managedCol) {
1813
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1814
                            $managedCol->setOwner($managedCopy, $assoc2);
1815
                            $prop->setValue($managedCopy, $managedCol);
1816
                            $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
                        if ($assoc2['isCascadeMerge']) {
1824
                            $managedCol->initialize();
1825
1826
                            // If $managedCol differs from the merged collection, clear and set dirty
1827
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1828
                                $managedCol->unwrap()->clear();
1829
                                $managedCol->setDirty(true);
1830
1831
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1832
                                    $this->scheduleForSynchronization($managedCopy);
1833
                                }
1834
                            }
1835
                        }
1836
                    }
1837
                }
1838
1839
                if (! $class->isChangeTrackingNotify()) {
1840
                    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
            if ($class->isChangeTrackingDeferredExplicit()) {
1848
                $this->scheduleForSynchronization($document);
1849
            }
1850
        }
1851
1852
        if ($prevManagedCopy !== null) {
1853
            $assocField = $assoc['fieldName'];
1854
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1855
1856
            if ($assoc['type'] === 'one') {
1857
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1858
            } else {
1859
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1860
1861
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1862
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1863
                }
1864
            }
1865
        }
1866
1867
        // Mark the managed copy visited as well
1868
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1869
1870
        $this->cascadeMerge($document, $managedCopy, $visited);
1871
1872
        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 2
    public function detach(object $document) : void
1882
    {
1883 2
        $visited = [];
1884 2
        $this->doDetach($document, $visited);
1885 2
    }
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 3
    private function doDetach(object $document, array &$visited) : void
1893
    {
1894 3
        $oid = spl_object_hash($document);
1895 3
        if (isset($visited[$oid])) {
1896
            return; // Prevent infinite recursion
1897
        }
1898
1899 3
        $visited[$oid] = $document; // mark visited
1900
1901 3
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1902 3
            case self::STATE_MANAGED:
1903 3
                $this->removeFromIdentityMap($document);
1904
                unset(
1905 3
                    $this->documentInsertions[$oid],
1906 3
                    $this->documentUpdates[$oid],
1907 3
                    $this->documentDeletions[$oid],
1908 3
                    $this->documentIdentifiers[$oid],
1909 3
                    $this->documentStates[$oid],
1910 3
                    $this->originalDocumentData[$oid],
1911 3
                    $this->parentAssociations[$oid],
1912 3
                    $this->documentUpserts[$oid],
1913 3
                    $this->hasScheduledCollections[$oid],
1914 3
                    $this->embeddedDocumentsRegistry[$oid]
1915
                );
1916 3
                break;
1917
            case self::STATE_NEW:
1918
            case self::STATE_DETACHED:
1919
                return;
1920
        }
1921
1922 3
        $this->cascadeDetach($document, $visited);
1923 3
    }
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
    public function refresh(object $document) : void
1934
    {
1935
        $visited = [];
1936
        $this->doRefresh($document, $visited);
1937
    }
1938
1939
    /**
1940
     * Executes a refresh operation on a document.
1941
     *
1942
     * @throws InvalidArgumentException If the document is not MANAGED.
1943
     */
1944
    private function doRefresh(object $document, array &$visited) : void
1945
    {
1946
        $oid = spl_object_hash($document);
1947
        if (isset($visited[$oid])) {
1948
            return; // Prevent infinite recursion
1949
        }
1950
1951
        $visited[$oid] = $document; // mark visited
1952
1953
        $class = $this->dm->getClassMetadata(get_class($document));
1954
1955
        if (! $class->isEmbeddedDocument) {
1956
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1957
                throw new InvalidArgumentException('Document is not MANAGED.');
1958
            }
1959
1960
            $this->getDocumentPersister($class->name)->refresh($document);
1961
        }
1962
1963
        $this->cascadeRefresh($document, $visited);
1964
    }
1965
1966
    /**
1967
     * Cascades a refresh operation to associated documents.
1968
     */
1969
    private function cascadeRefresh(object $document, array &$visited) : void
1970
    {
1971
        $class = $this->dm->getClassMetadata(get_class($document));
1972
1973
        $associationMappings = array_filter(
1974
            $class->associationMappings,
1975
            static function ($assoc) {
1976
                return $assoc['isCascadeRefresh'];
1977
            }
1978
        );
1979
1980
        foreach ($associationMappings as $mapping) {
1981
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1982
            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
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1984
                    // Unwrap so that foreach() does not initialize
1985
                    $relatedDocuments = $relatedDocuments->unwrap();
1986
                }
1987
                foreach ($relatedDocuments as $relatedDocument) {
1988
                    $this->doRefresh($relatedDocument, $visited);
1989
                }
1990
            } elseif ($relatedDocuments !== null) {
1991
                $this->doRefresh($relatedDocuments, $visited);
1992
            }
1993
        }
1994
    }
1995
1996
    /**
1997
     * Cascades a detach operation to associated documents.
1998
     */
1999 3
    private function cascadeDetach(object $document, array &$visited) : void
2000
    {
2001 3
        $class = $this->dm->getClassMetadata(get_class($document));
2002 3
        foreach ($class->fieldMappings as $mapping) {
2003 3
            if (! $mapping['isCascadeDetach']) {
2004 3
                continue;
2005
            }
2006 3
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2007 3
            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 3
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2009
                    // Unwrap so that foreach() does not initialize
2010
                    $relatedDocuments = $relatedDocuments->unwrap();
2011
                }
2012 3
                foreach ($relatedDocuments as $relatedDocument) {
2013 3
                    $this->doDetach($relatedDocument, $visited);
2014
                }
2015 3
            } elseif ($relatedDocuments !== null) {
2016 3
                $this->doDetach($relatedDocuments, $visited);
2017
            }
2018
        }
2019 3
    }
2020
2021
    /**
2022
     * Cascades a merge operation to associated documents.
2023
     */
2024
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2025
    {
2026
        $class = $this->dm->getClassMetadata(get_class($document));
2027
2028
        $associationMappings = array_filter(
2029
            $class->associationMappings,
2030
            static function ($assoc) {
2031
                return $assoc['isCascadeMerge'];
2032
            }
2033
        );
2034
2035
        foreach ($associationMappings as $assoc) {
2036
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2037
2038
            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
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2040
                    // Collections are the same, so there is nothing to do
2041
                    continue;
2042
                }
2043
2044
                foreach ($relatedDocuments as $relatedDocument) {
2045
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2046
                }
2047
            } elseif ($relatedDocuments !== null) {
2048
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2049
            }
2050
        }
2051
    }
2052
2053
    /**
2054
     * Cascades the save operation to associated documents.
2055
     */
2056 73
    private function cascadePersist(object $document, array &$visited) : void
2057
    {
2058 73
        $class = $this->dm->getClassMetadata(get_class($document));
2059
2060 73
        $associationMappings = array_filter(
2061 73
            $class->associationMappings,
2062
            static function ($assoc) {
2063 64
                return $assoc['isCascadePersist'];
2064 73
            }
2065
        );
2066
2067 73
        foreach ($associationMappings as $fieldName => $mapping) {
2068 60
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2069
2070 60
            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 32
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2072
                    if ($relatedDocuments->getOwner() !== $document) {
2073
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2074
                    }
2075
                    // Unwrap so that foreach() does not initialize
2076
                    $relatedDocuments = $relatedDocuments->unwrap();
2077
                }
2078
2079 32
                $count = 0;
2080 32
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2081 14
                    if (! empty($mapping['embedded'])) {
2082 11
                        [, $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 11
                        if ($knownParent && $knownParent !== $document) {
2084
                            $relatedDocument               = clone $relatedDocument;
2085
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2086
                        }
2087 11
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2088 11
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2089
                    }
2090 14
                    $this->doPersist($relatedDocument, $visited);
2091
                }
2092 54
            } elseif ($relatedDocuments !== null) {
2093 23
                if (! empty($mapping['embedded'])) {
2094 10
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2095 10
                    if ($knownParent && $knownParent !== $document) {
2096
                        $relatedDocuments = clone $relatedDocuments;
2097
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2098
                    }
2099 10
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2100
                }
2101 23
                $this->doPersist($relatedDocuments, $visited);
2102
            }
2103
        }
2104 71
    }
2105
2106
    /**
2107
     * Cascades the delete operation to associated documents.
2108
     */
2109
    private function cascadeRemove(object $document, array &$visited) : void
2110
    {
2111
        $class = $this->dm->getClassMetadata(get_class($document));
2112
        foreach ($class->fieldMappings as $mapping) {
2113
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2114
                continue;
2115
            }
2116
            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
                $document->initializeProxy();
2118
            }
2119
2120
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2121
            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
                foreach ($relatedDocuments as $relatedDocument) {
2124
                    $this->doRemove($relatedDocument, $visited);
2125
                }
2126
            } elseif ($relatedDocuments !== null) {
2127
                $this->doRemove($relatedDocuments, $visited);
2128
            }
2129
        }
2130
    }
2131
2132
    /**
2133
     * Acquire a lock on the given document.
2134
     *
2135
     * @internal
2136
     *
2137
     * @throws LockException
2138
     * @throws InvalidArgumentException
2139
     */
2140 1
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2141
    {
2142 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2143 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2144
        }
2145
2146
        $documentName = get_class($document);
2147
        $class        = $this->dm->getClassMetadata($documentName);
2148
2149
        if ($lockMode === LockMode::OPTIMISTIC) {
2150
            if (! $class->isVersioned) {
2151
                throw LockException::notVersioned($documentName);
2152
            }
2153
2154
            if ($lockVersion !== null) {
2155
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2156
                if ($documentVersion !== $lockVersion) {
2157
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2158
                }
2159
            }
2160
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2161
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2162
        }
2163
    }
2164
2165
    /**
2166
     * Releases a lock on the given document.
2167
     *
2168
     * @internal
2169
     *
2170
     * @throws InvalidArgumentException
2171
     */
2172
    public function unlock(object $document) : void
2173
    {
2174
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2175
            throw new InvalidArgumentException('Document is not MANAGED.');
2176
        }
2177
        $documentName = get_class($document);
2178
        $this->getDocumentPersister($documentName)->unlock($document);
2179
    }
2180
2181
    /**
2182
     * Clears the UnitOfWork.
2183
     *
2184
     * @internal
2185
     */
2186 8
    public function clear(?string $documentName = null) : void
2187
    {
2188 8
        if ($documentName === null) {
2189 7
            $this->identityMap                 =
2190 7
            $this->documentIdentifiers         =
2191 7
            $this->originalDocumentData        =
2192 7
            $this->documentChangeSets          =
2193 7
            $this->documentStates              =
2194 7
            $this->scheduledForSynchronization =
2195 7
            $this->documentInsertions          =
2196 7
            $this->documentUpserts             =
2197 7
            $this->documentUpdates             =
2198 7
            $this->documentDeletions           =
2199 7
            $this->collectionUpdates           =
2200 7
            $this->collectionDeletions         =
2201 7
            $this->parentAssociations          =
2202 7
            $this->embeddedDocumentsRegistry   =
2203 7
            $this->orphanRemovals              =
2204 7
            $this->hasScheduledCollections     = [];
2205
        } else {
2206 1
            $visited = [];
2207 1
            foreach ($this->identityMap as $className => $documents) {
2208 1
                if ($className !== $documentName) {
2209 1
                    continue;
2210
                }
2211
2212 1
                foreach ($documents as $document) {
2213 1
                    $this->doDetach($document, $visited);
2214
                }
2215
            }
2216
        }
2217
2218 8
        if (! $this->evm->hasListeners(Events::onClear)) {
2219 8
            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
    public function scheduleOrphanRemoval(object $document) : void
2233
    {
2234
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2235
    }
2236
2237
    /**
2238
     * Unschedules an embedded or referenced object for removal.
2239
     *
2240
     * @internal
2241
     */
2242 2
    public function unscheduleOrphanRemoval(object $document) : void
2243
    {
2244 2
        $oid = spl_object_hash($document);
2245 2
        unset($this->orphanRemovals[$oid]);
2246 2
    }
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
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2256
    {
2257
        $owner = $coll->getOwner();
2258
        if ($owner === null) { // cloned
2259
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2260
        } elseif ($owner !== $document) { // no clone, we have to fix
2261
            if (! $coll->isInitialized()) {
2262
                $coll->initialize(); // we have to do this otherwise the cols share state
2263
            }
2264
            $newValue = clone $coll;
2265
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2266
            $class->reflFields[$propName]->setValue($document, $newValue);
2267
            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
            return $newValue;
2273
        }
2274
2275
        return $coll;
2276
    }
2277
2278
    /**
2279
     * Schedules a complete collection for removal when this UnitOfWork commits.
2280
     *
2281
     * @internal
2282
     */
2283
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2284
    {
2285
        $oid = spl_object_hash($coll);
2286
        unset($this->collectionUpdates[$oid]);
2287
        if (isset($this->collectionDeletions[$oid])) {
2288
            return;
2289
        }
2290
2291
        $this->collectionDeletions[$oid] = $coll;
2292
        $this->scheduleCollectionOwner($coll);
2293
    }
2294
2295
    /**
2296
     * Checks whether a PersistentCollection is scheduled for deletion.
2297
     *
2298
     * @internal
2299
     */
2300 1
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2301
    {
2302 1
        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 3
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2311
    {
2312 3
        if ($coll->getOwner() === null) {
2313
            return;
2314
        }
2315
2316 3
        $oid = spl_object_hash($coll);
2317 3
        if (! isset($this->collectionDeletions[$oid])) {
2318 3
            return;
2319
        }
2320
2321
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2322
        unset($this->collectionDeletions[$oid]);
2323
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2324
    }
2325
2326
    /**
2327
     * Schedules a collection for update when this UnitOfWork commits.
2328
     *
2329
     * @internal
2330
     */
2331 9
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2332
    {
2333 9
        $mapping = $coll->getMapping();
2334 9
        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
            $this->unscheduleCollectionDeletion($coll);
2339
        }
2340 9
        $oid = spl_object_hash($coll);
2341 9
        if (isset($this->collectionUpdates[$oid])) {
2342
            return;
2343
        }
2344
2345 9
        $this->collectionUpdates[$oid] = $coll;
2346 9
        $this->scheduleCollectionOwner($coll);
2347 9
    }
2348
2349
    /**
2350
     * Unschedules a collection from being updated when this UnitOfWork commits.
2351
     *
2352
     * @internal
2353
     */
2354 3
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2355
    {
2356 3
        if ($coll->getOwner() === null) {
2357
            return;
2358
        }
2359
2360 3
        $oid = spl_object_hash($coll);
2361 3
        if (! isset($this->collectionUpdates[$oid])) {
2362
            return;
2363
        }
2364
2365 3
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2366 3
        unset($this->collectionUpdates[$oid]);
2367 3
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2368 3
    }
2369
2370
    /**
2371
     * Checks whether a PersistentCollection is scheduled for update.
2372
     *
2373
     * @internal
2374
     */
2375
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2376
    {
2377
        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
    public function getVisitedCollections(object $document) : array
2389
    {
2390
        $oid = spl_object_hash($document);
2391
2392
        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
    public function getScheduledCollections(object $document) : array
2403
    {
2404
        $oid = spl_object_hash($document);
2405
2406
        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
    public function hasScheduledCollections(object $document) : bool
2416
    {
2417
        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 9
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2432
    {
2433 9
        if ($coll->getOwner() === null) {
2434
            return;
2435
        }
2436
2437 9
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2438 9
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2439
2440 9
        if ($document !== $coll->getOwner()) {
2441
            $parent  = $coll->getOwner();
2442
            $mapping = [];
2443
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2444
                [$mapping, $parent ] = $parentAssoc;
2445
            }
2446
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2447
                $class            = $this->dm->getClassMetadata(get_class($document));
2448
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2449
                $this->scheduleCollectionUpdate($atomicCollection);
2450
                $this->unscheduleCollectionDeletion($coll);
2451
                $this->unscheduleCollectionUpdate($coll);
2452
            }
2453
        }
2454
2455 9
        if ($this->isDocumentScheduled($document)) {
2456 9
            return;
2457
        }
2458
2459
        $this->scheduleForUpdate($document);
2460
    }
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 9
    public function getOwningDocument(object $document) : object
2472
    {
2473 9
        $class = $this->dm->getClassMetadata(get_class($document));
2474 9
        while ($class->isEmbeddedDocument) {
2475
            $parentAssociation = $this->getParentAssociation($document);
2476
2477
            if (! $parentAssociation) {
2478
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2479
            }
2480
2481
            [, $document ] = $parentAssociation;
2482
            $class         = $this->dm->getClassMetadata(get_class($document));
2483
        }
2484
2485 9
        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 6
    public function getClassNameForAssociation(array $mapping, $data) : string
2497
    {
2498 6
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2499
2500 6
        $discriminatorValue = null;
2501 6
        if (isset($discriminatorField, $data[$discriminatorField])) {
2502 1
            $discriminatorValue = $data[$discriminatorField];
2503 5
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2504
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2505
        }
2506
2507 6
        if ($discriminatorValue !== null) {
2508 1
            return $mapping['discriminatorMap'][$discriminatorValue]
2509 1
                ?? (string) $discriminatorValue;
2510
        }
2511
2512 5
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2513
2514 5
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2515 1
            $discriminatorValue = $data[$class->discriminatorField];
2516 4
        } elseif ($class->defaultDiscriminatorValue !== null) {
2517
            $discriminatorValue = $class->defaultDiscriminatorValue;
2518
        }
2519
2520 5
        if ($discriminatorValue !== null) {
2521 1
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2522
        }
2523
2524 4
        return $mapping['targetDocument'];
2525
    }
2526
2527
    /**
2528
     * Creates a document. Used for reconstitution of documents during hydration.
2529
     *
2530
     * @internal
2531
     */
2532 1
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2533
    {
2534 1
        $class = $this->dm->getClassMetadata($className);
2535
2536
        // @TODO figure out how to remove this
2537 1
        $discriminatorValue = null;
2538 1
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2539
            $discriminatorValue = $data[$class->discriminatorField];
2540 1
        } elseif (isset($class->defaultDiscriminatorValue)) {
2541
            $discriminatorValue = $class->defaultDiscriminatorValue;
2542
        }
2543
2544 1
        if ($discriminatorValue !== null) {
2545
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2546
2547
            $class = $this->dm->getClassMetadata($className);
2548
2549
            unset($data[$class->discriminatorField]);
2550
        }
2551
2552 1
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2553
            $document = $class->newInstance();
2554
            $this->hydratorFactory->hydrate($document, $data, $hints);
2555
2556
            return $document;
2557
        }
2558
2559 1
        $isManagedObject = false;
2560 1
        $serializedId    = null;
2561 1
        $id              = null;
2562 1
        if (! $class->isQueryResultDocument) {
2563 1
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2564 1
            $serializedId    = serialize($id);
2565 1
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2566
        }
2567
2568 1
        $oid = null;
2569 1
        if ($isManagedObject) {
2570
            $document = $this->identityMap[$class->name][$serializedId];
2571
            $oid      = spl_object_hash($document);
2572
            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
                $document->setProxyInitializer(null);
2574
                $overrideLocalValues = true;
2575
                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
                    $document->addPropertyChangedListener($this);
2577
                }
2578
            } else {
2579
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2580
            }
2581
            if ($overrideLocalValues) {
2582
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2583
                $this->originalDocumentData[$oid] = $data;
2584
            }
2585
        } else {
2586 1
            if ($document === null) {
2587 1
                $document = $class->newInstance();
2588
            }
2589
2590 1
            if (! $class->isQueryResultDocument) {
2591 1
                $this->registerManaged($document, $id, $data);
2592 1
                $oid                                            = spl_object_hash($document);
2593 1
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2594 1
                $this->identityMap[$class->name][$serializedId] = $document;
2595
            }
2596
2597 1
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2598
2599
            if (! $class->isQueryResultDocument) {
2600
                $this->originalDocumentData[$oid] = $data;
2601
            }
2602
        }
2603
2604
        return $document;
2605
    }
2606
2607
    /**
2608
     * Initializes (loads) an uninitialized persistent collection of a document.
2609
     *
2610
     * @internal
2611
     */
2612 3
    public function loadCollection(PersistentCollectionInterface $collection) : void
2613
    {
2614 3
        if ($collection->getOwner() === null) {
2615
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2616
        }
2617
2618 3
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2619 2
        $this->lifecycleEventManager->postCollectionLoad($collection);
2620 2
    }
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
    public function getOriginalDocumentData(object $document) : array
2639
    {
2640
        $oid = spl_object_hash($document);
2641
2642
        return $this->originalDocumentData[$oid] ?? [];
2643
    }
2644
2645
    /**
2646
     * @internal
2647
     */
2648
    public function setOriginalDocumentData(object $document, array $data) : void
2649
    {
2650
        $oid                              = spl_object_hash($document);
2651
        $this->originalDocumentData[$oid] = $data;
2652
        unset($this->documentChangeSets[$oid]);
2653
    }
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
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2663
    {
2664
        $this->originalDocumentData[$oid][$property] = $value;
2665
    }
2666
2667
    /**
2668
     * Gets the identifier of a document.
2669
     *
2670
     * @return mixed The identifier value
2671
     */
2672 44
    public function getDocumentIdentifier(object $document)
2673
    {
2674 44
        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 1
    public function size() : int
2696
    {
2697 1
        $count = 0;
2698 1
        foreach ($this->identityMap as $documentSet) {
2699 1
            $count += count($documentSet);
2700
        }
2701
2702 1
        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 9
    public function registerManaged(object $document, $id, array $data) : void
2720
    {
2721 9
        $oid   = spl_object_hash($document);
2722 9
        $class = $this->dm->getClassMetadata(get_class($document));
2723
2724 9
        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 4
            $this->documentIdentifiers[$oid] = $oid;
2726
        } else {
2727 6
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2728
        }
2729
2730 9
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2731 9
        $this->originalDocumentData[$oid] = $data;
2732 9
        $this->addToIdentityMap($document);
2733 9
    }
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
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2756
    {
2757
        $oid   = spl_object_hash($document);
2758
        $class = $this->dm->getClassMetadata(get_class($document));
2759
2760
        if (! isset($class->fieldMappings[$propertyName])) {
2761
            return; // ignore non-persistent fields
2762
        }
2763
2764
        // Update changeset and mark document for synchronization
2765
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2766
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2767
            return;
2768
        }
2769
2770
        $this->scheduleForSynchronization($document);
2771
    }
2772
2773
    /**
2774
     * Gets the currently scheduled document insertions in this UnitOfWork.
2775
     */
2776
    public function getScheduledDocumentInsertions() : array
2777
    {
2778
        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
    public function getScheduledDocumentUpdates() : array
2793
    {
2794
        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