Completed
Push — master ( 70d125...9aae6d )
by Andreas
24s queued 11s
created

UnitOfWork::scheduleForSynchronization()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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
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
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268 1651
    {
269
        $this->dm                    = $dm;
270 1651
        $this->evm                   = $evm;
271 1651
        $this->hydratorFactory       = $hydratorFactory;
272 1651
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1651
    }
274 1651
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     */
279
    public function getPersistenceBuilder() : PersistenceBuilder
280 1130
    {
281
        if (! $this->persistenceBuilder) {
282 1130
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
283 1130
        }
284
        return $this->persistenceBuilder;
285 1130
    }
286
287
    /**
288
     * Sets the parent association for a given embedded document.
289
     */
290
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
291 209
    {
292
        $oid                                   = spl_object_hash($document);
293 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
294 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
295 209
    }
296 209
297
    /**
298
     * Gets the parent association for a given embedded document.
299
     *
300
     *     <code>
301
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
302
     *     </code>
303
     */
304
    public function getParentAssociation(object $document) : ?array
305 233
    {
306
        $oid = spl_object_hash($document);
307 233
308
        return $this->parentAssociations[$oid] ?? null;
309 233
    }
310
311
    /**
312
     * Get the document persister instance for the given document name
313
     */
314
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
315 1128
    {
316
        if (! isset($this->persisters[$documentName])) {
317 1128
            $class                           = $this->dm->getClassMetadata($documentName);
318 1115
            $pb                              = $this->getPersistenceBuilder();
319 1115
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
320 1115
        }
321
        return $this->persisters[$documentName];
322 1128
    }
323
324
    /**
325
     * Get the collection persister instance.
326
     */
327
    public function getCollectionPersister() : CollectionPersister
328 1128
    {
329
        if (! isset($this->collectionPersister)) {
330 1128
            $pb                        = $this->getPersistenceBuilder();
331 1128
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
332 1128
        }
333
        return $this->collectionPersister;
334 1128
    }
335
336
    /**
337
     * Set the document persister instance to use for the given document name
338
     */
339
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
340 13
    {
341
        $this->persisters[$documentName] = $persister;
342 13
    }
343 13
344
    /**
345
     * Commits the UnitOfWork, executing all operations that have been postponed
346
     * up to this point. The state of all managed documents will be synchronized with
347
     * the database.
348
     *
349
     * The operations are executed in the following order:
350
     *
351
     * 1) All document insertions
352
     * 2) All document updates
353
     * 3) All document deletions
354
     *
355
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
356
     */
357
    public function commit(array $options = []) : void
358 611
    {
359
        // Raise preFlush
360
        if ($this->evm->hasListeners(Events::preFlush)) {
361 611
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
362
        }
363
364
        // Compute changes done since last commit.
365
        $this->computeChangeSets();
366 611
367
        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...
368 610
            $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...
369 257
            $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...
370 215
            $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...
371 198
            $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...
372 22
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
373 22
            $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...
374 610
        ) {
375
            return; // Nothing to do.
376 22
        }
377
378
        $this->commitsInProgress++;
379 607
        if ($this->commitsInProgress > 1) {
380 607
            throw MongoDBException::commitInProgress();
381
        }
382
        try {
383
            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...
384 607
                foreach ($this->orphanRemovals as $removal) {
385 55
                    $this->remove($removal);
386 55
                }
387
            }
388
389
            // Raise onFlush
390
            if ($this->evm->hasListeners(Events::onFlush)) {
391 607
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
392 5
            }
393
394
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
395 606
                [$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...
396 86
                $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...
397 86
            }
398
399
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
400 606
                [$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...
401 530
                $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...
402 530
            }
403
404
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
405 593
                [$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...
406 236
                $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...
407 236
            }
408
409
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
410 593
                [$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...
411 79
                $this->executeDeletions($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
412 79
            }
413
414
            // Raise postFlush
415
            if ($this->evm->hasListeners(Events::postFlush)) {
416 593
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
417
            }
418
419
            // Clear up
420
            $this->documentInsertions          =
421 593
            $this->documentUpserts             =
422 593
            $this->documentUpdates             =
423 593
            $this->documentDeletions           =
424 593
            $this->documentChangeSets          =
425 593
            $this->collectionUpdates           =
426 593
            $this->collectionDeletions         =
427 593
            $this->visitedCollections          =
428 593
            $this->scheduledForSynchronization =
429 593
            $this->orphanRemovals              =
430 593
            $this->hasScheduledCollections     = [];
431 593
        } finally {
432 593
            $this->commitsInProgress--;
433 607
        }
434
    }
435 593
436
    /**
437
     * Groups a list of scheduled documents by their class.
438
     */
439
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
440 606
    {
441
        if (empty($documents)) {
442 606
            return [];
443 606
        }
444
        $divided = [];
445 605
        $embeds  = [];
446 605
        foreach ($documents as $oid => $d) {
447 605
            $className = get_class($d);
448 605
            if (isset($embeds[$className])) {
449 605
                continue;
450 81
            }
451
            if (isset($divided[$className])) {
452 605
                $divided[$className][1][$oid] = $d;
453 165
                continue;
454 165
            }
455
            $class = $this->dm->getClassMetadata($className);
456 605
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
457 605
                $embeds[$className] = true;
458 183
                continue;
459 183
            }
460
            if (empty($divided[$class->name])) {
461 605
                $divided[$class->name] = [$class, [$oid => $d]];
462 605
            } else {
463
                $divided[$class->name][1][$oid] = $d;
464 4
            }
465
        }
466
        return $divided;
467 605
    }
468
469
    /**
470
     * Compute changesets of all documents scheduled for insertion.
471
     *
472
     * Embedded documents will not be processed.
473
     */
474
    private function computeScheduleInsertsChangeSets() : void
475 616
    {
476
        foreach ($this->documentInsertions as $document) {
477 616
            $class = $this->dm->getClassMetadata(get_class($document));
478 543
            if ($class->isEmbeddedDocument) {
479 543
                continue;
480 165
            }
481
482
            $this->computeChangeSet($class, $document);
483 537
        }
484
    }
485 615
486
    /**
487
     * Compute changesets of all documents scheduled for upsert.
488
     *
489
     * Embedded documents will not be processed.
490
     */
491
    private function computeScheduleUpsertsChangeSets() : void
492 615
    {
493
        foreach ($this->documentUpserts as $document) {
494 615
            $class = $this->dm->getClassMetadata(get_class($document));
495 85
            if ($class->isEmbeddedDocument) {
496 85
                continue;
497
            }
498
499
            $this->computeChangeSet($class, $document);
500 85
        }
501
    }
502 615
503
    /**
504
     * Gets the changeset for a document.
505
     *
506
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
507
     */
508
    public function getDocumentChangeSet(object $document) : array
509 611
    {
510
        $oid = spl_object_hash($document);
511 611
512
        return $this->documentChangeSets[$oid] ?? [];
513 611
    }
514
515
    /**
516
     * INTERNAL:
517
     * Sets the changeset for a document.
518
     */
519
    public function setDocumentChangeSet(object $document, array $changeset) : void
520 1
    {
521
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
522 1
    }
523 1
524
    /**
525
     * Get a documents actual data, flattening all the objects to arrays.
526
     *
527
     * @return array
528
     */
529
    public function getDocumentActualData(object $document) : array
530 616
    {
531
        $class      = $this->dm->getClassMetadata(get_class($document));
532 616
        $actualData = [];
533 616
        foreach ($class->reflFields as $name => $refProp) {
534 616
            $mapping = $class->fieldMappings[$name];
535 616
            // skip not saved fields
536
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
537 616
                continue;
538 197
            }
539
            $value = $refProp->getValue($document);
540 616
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
541 616
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
542 616
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
543
                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...
544 400
                    $value = new ArrayCollection($value);
545 147
                }
546
547
                // Inject PersistentCollection
548
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
549 400
                $coll->setOwner($document, $mapping);
550 400
                $coll->setDirty(! $value->isEmpty());
551 400
                $class->reflFields[$name]->setValue($document, $coll);
552 400
                $actualData[$name] = $coll;
553 400
            } else {
554
                $actualData[$name] = $value;
555 616
            }
556
        }
557
        return $actualData;
558 616
    }
559
560
    /**
561
     * Computes the changes that happened to a single document.
562
     *
563
     * Modifies/populates the following properties:
564
     *
565
     * {@link originalDocumentData}
566
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
567
     * then it was not fetched from the database and therefore we have no original
568
     * document data yet. All of the current document data is stored as the original document data.
569
     *
570
     * {@link documentChangeSets}
571
     * The changes detected on all properties of the document are stored there.
572
     * A change is a tuple array where the first entry is the old value and the second
573
     * entry is the new value of the property. Changesets are used by persisters
574
     * to INSERT/UPDATE the persistent document state.
575
     *
576
     * {@link documentUpdates}
577
     * If the document is already fully MANAGED (has been fetched from the database before)
578
     * and any changes to its properties are detected, then a reference to the document is stored
579
     * there to mark it for an update.
580
     */
581
    public function computeChangeSet(ClassMetadata $class, object $document) : void
582 612
    {
583
        if (! $class->isInheritanceTypeNone()) {
584 612
            $class = $this->dm->getClassMetadata(get_class($document));
585 183
        }
586
587
        // Fire PreFlush lifecycle callbacks
588
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
589 612
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
590 11
        }
591
592
        $this->computeOrRecomputeChangeSet($class, $document);
593 612
    }
594 611
595
    /**
596
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
597
     */
598
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
599 612
    {
600
        $oid           = spl_object_hash($document);
601 612
        $actualData    = $this->getDocumentActualData($document);
602 612
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
603 612
        if ($isNewDocument) {
604 612
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
605
            // These result in an INSERT.
606
            $this->originalDocumentData[$oid] = $actualData;
607 611
            $changeSet                        = [];
608 611
            foreach ($actualData as $propName => $actualValue) {
609 611
                /* At this PersistentCollection shouldn't be here, probably it
610
                 * was cloned and its ownership must be fixed
611
                 */
612
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
613 611
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
614
                    $actualValue           = $actualData[$propName];
615
                }
616
                // ignore inverse side of reference relationship
617
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
618 611
                    continue;
619 187
                }
620
                $changeSet[$propName] = [null, $actualValue];
621 611
            }
622
            $this->documentChangeSets[$oid] = $changeSet;
623 611
        } else {
624
            if ($class->isReadOnly) {
625 297
                return;
626 2
            }
627
            // Document is "fully" MANAGED: it was already fully persisted before
628
            // and we have a copy of the original data
629
            $originalData           = $this->originalDocumentData[$oid];
630 295
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
631 295
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
632 295
                $changeSet = $this->documentChangeSets[$oid];
633 2
            } else {
634
                $changeSet = [];
635 295
            }
636
637
            $gridFSMetadataProperty = null;
638 295
639
            if ($class->isFile) {
640 295
                try {
641
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
642 4
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
643 3
                } catch (MappingException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
644 1
                }
645
            }
646
647
            foreach ($actualData as $propName => $actualValue) {
648 295
                // skip not saved fields
649
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
650 295
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
651 295
                    continue;
652 4
                }
653
654
                $orgValue = $originalData[$propName] ?? null;
655 294
656
                // skip if value has not changed
657
                if ($orgValue === $actualValue) {
658 294
                    if (! $actualValue instanceof PersistentCollectionInterface) {
659 292
                        continue;
660 292
                    }
661
662
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
663 206
                        // consider dirty collections as changed as well
664
                        continue;
665 182
                    }
666
                }
667
668
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
669
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
670 254
                    if ($orgValue !== null) {
671 14
                        $this->scheduleOrphanRemoval($orgValue);
672 8
                    }
673
                    $changeSet[$propName] = [$orgValue, $actualValue];
674 14
                    continue;
675 14
                }
676
677
                // if owning side of reference-one relationship
678
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
679 247
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
680 13
                        $this->scheduleOrphanRemoval($orgValue);
681 1
                    }
682
683
                    $changeSet[$propName] = [$orgValue, $actualValue];
684 13
                    continue;
685 13
                }
686
687
                if ($isChangeTrackingNotify) {
688 240
                    continue;
689 3
                }
690
691
                // ignore inverse side of reference relationship
692
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
693 238
                    continue;
694 6
                }
695
696
                // Persistent collection was exchanged with the "originally"
697
                // created one. This can only mean it was cloned and replaced
698
                // on another document.
699
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
700 236
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
701 6
                }
702
703
                // if embed-many or reference-many relationship
704
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
705 236
                    $changeSet[$propName] = [$orgValue, $actualValue];
706 125
                    /* If original collection was exchanged with a non-empty value
707
                     * and $set will be issued, there is no need to $unset it first
708
                     */
709
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
710 125
                        continue;
711 31
                    }
712
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
713 104
                        $this->scheduleCollectionDeletion($orgValue);
714 18
                    }
715
                    continue;
716 104
                }
717
718
                // skip equivalent date values
719
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
720 148
                    /** @var DateType $dateType */
721
                    $dateType      = Type::getType('date');
722 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
723 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
724 37
725
                    $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...
726 37
                    $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
727 37
728
                    if ($orgTimestamp === $actualTimestamp) {
729 37
                        continue;
730 30
                    }
731
                }
732
733
                // regular field
734
                $changeSet[$propName] = [$orgValue, $actualValue];
735 131
            }
736
            if ($changeSet) {
737 295
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
738 243
                    ? $changeSet + $this->documentChangeSets[$oid]
739 19
                    : $changeSet;
740 240
741
                $this->originalDocumentData[$oid] = $actualData;
742 243
                $this->scheduleForUpdate($document);
743 243
            }
744
        }
745
746
        // Look for changes in associations of the document
747
        $associationMappings = array_filter(
748 612
            $class->associationMappings,
749 612
            static function ($assoc) {
750
                return empty($assoc['notSaved']);
751 471
            }
752 612
        );
753
754
        foreach ($associationMappings as $mapping) {
755 612
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
756 471
757
            if ($value === null) {
758 471
                continue;
759 323
            }
760
761
            $this->computeAssociationChanges($document, $mapping, $value);
762 452
763
            if (isset($mapping['reference'])) {
764 451
                continue;
765 337
            }
766
767
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
768 354
769
            foreach ($values as $obj) {
770 354
                $oid2 = spl_object_hash($obj);
771 188
772
                if (isset($this->documentChangeSets[$oid2])) {
773 188
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
774 186
                        // instance of $value is the same as it was previously otherwise there would be
775
                        // change set already in place
776
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
777 42
                    }
778
779
                    if (! $isNewDocument) {
780 186
                        $this->scheduleForUpdate($document);
781 87
                    }
782
783
                    break;
784 186
                }
785
            }
786
        }
787
    }
788 611
789
    /**
790
     * Computes all the changes that have been done to documents and collections
791
     * since the last commit and stores these changes in the _documentChangeSet map
792
     * temporarily for access by the persisters, until the UoW commit is finished.
793
     */
794
    public function computeChangeSets() : void
795 616
    {
796
        $this->computeScheduleInsertsChangeSets();
797 616
        $this->computeScheduleUpsertsChangeSets();
798 615
799
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
800
        foreach ($this->identityMap as $className => $documents) {
801 615
            $class = $this->dm->getClassMetadata($className);
802 615
            if ($class->isEmbeddedDocument) {
803 615
                /* we do not want to compute changes to embedded documents up front
804
                 * in case embedded document was replaced and its changeset
805
                 * would corrupt data. Embedded documents' change set will
806
                 * be calculated by reachability from owning document.
807
                 */
808
                continue;
809 178
            }
810
811
            // If change tracking is explicit or happens through notification, then only compute
812
            // changes on document of that type that are explicitly marked for synchronization.
813
            switch (true) {
814
                case $class->isChangeTrackingDeferredImplicit():
815 615
                    $documentsToProcess = $documents;
816 614
                    break;
817 614
818
                case isset($this->scheduledForSynchronization[$className]):
819 4
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
820 3
                    break;
821 3
822
                default:
823
                    $documentsToProcess = [];
824 4
            }
825
826
            foreach ($documentsToProcess as $document) {
827 615
                // Ignore uninitialized proxy objects
828
                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...
829 610
                    continue;
830 9
                }
831
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
832
                $oid = spl_object_hash($document);
833 610
                if (isset($this->documentInsertions[$oid])
834 610
                    || isset($this->documentUpserts[$oid])
835 338
                    || isset($this->documentDeletions[$oid])
836 292
                    || ! isset($this->documentStates[$oid])
837 610
                ) {
838
                    continue;
839 608
                }
840
841
                $this->computeChangeSet($class, $document);
842 292
            }
843
        }
844
    }
845 615
846
    /**
847
     * Computes the changes of an association.
848
     *
849
     * @param mixed $value The value of the association.
850
     *
851
     * @throws InvalidArgumentException
852
     */
853
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
854 452
    {
855
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
856 452
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
857 452
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
858 452
859
        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...
860 452
            return;
861 7
        }
862
863
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
864 451
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
865 258
                $this->scheduleCollectionUpdate($value);
866 254
            }
867
868
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
869 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
870 258
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
871 258
                $value->initialize();
872 151
                foreach ($value->getDeletedDocuments() as $orphan) {
873 151
                    $this->scheduleOrphanRemoval($orphan);
874 25
                }
875
            }
876
        }
877
878
        // Look through the documents, and in any of their associations,
879
        // for transient (new) documents, recursively. ("Persistence by reachability")
880
        // Unwrap. Uninitialized collections will simply be empty.
881
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
882 451
883
        $count = 0;
884 451
        foreach ($unwrappedValue as $key => $entry) {
885 451
            if (! is_object($entry)) {
886 367
                throw new InvalidArgumentException(
887 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
888 1
                );
889
            }
890
891
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
892 366
893
            $state = $this->getDocumentState($entry, self::STATE_NEW);
894 366
895
            // Handle "set" strategy for multi-level hierarchy
896
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
897 366
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
898 366
899
            $count++;
900 366
901
            switch ($state) {
902
                case self::STATE_NEW:
903 366
                    if (! $assoc['isCascadePersist']) {
904 70
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
905
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
906
                            . ' Explicitly persist the new document or configure cascading persist operations'
907
                            . ' on the relationship.');
908
                    }
909
910
                    $this->persistNew($targetClass, $entry);
911 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
912 70
                    $this->computeChangeSet($targetClass, $entry);
913 70
                    break;
914 70
915
                case self::STATE_MANAGED:
916 362
                    if ($targetClass->isEmbeddedDocument) {
917 362
                        [, $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...
918 179
                        if ($knownParent && $knownParent !== $parentDocument) {
919 179
                            $entry = clone $entry;
920 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
921 6
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
922 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
923 3
                                $poid = spl_object_hash($parentDocument);
924 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
925 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
926 3
                                }
927
                            } else {
928
                                // must use unwrapped value to not trigger orphan removal
929
                                $unwrappedValue[$key] = $entry;
930 4
                            }
931
                            $this->persistNew($targetClass, $entry);
932 6
                        }
933
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
934 179
                        $this->computeChangeSet($targetClass, $entry);
935 179
                    }
936
                    break;
937 362
938
                case self::STATE_REMOVED:
939 1
                    // Consume the $value as array (it's either an array or an ArrayAccess)
940
                    // and remove the element from Collection.
941
                    if ($assoc['type'] === ClassMetadata::MANY) {
942 1
                        unset($value[$key]);
943
                    }
944
                    break;
945 1
946
                case self::STATE_DETACHED:
947
                    // Can actually not happen right now as we assume STATE_NEW,
948
                    // so the exception will be raised from the DBAL layer (constraint violation).
949
                    throw new InvalidArgumentException('A detached document was found through a '
950
                        . 'relationship during cascading a persist operation.');
951
952
                default:
953
                    // MANAGED associated documents are already taken into account
954
                    // during changeset calculation anyway, since they are in the identity map.
955
            }
956
        }
957
    }
958 450
959
    /**
960
     * INTERNAL:
961
     * Computes the changeset of an individual document, independently of the
962
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
963
     *
964
     * The passed document must be a managed document. If the document already has a change set
965
     * because this method is invoked during a commit cycle then the change sets are added.
966
     * whereby changes detected in this method prevail.
967
     *
968
     * @throws InvalidArgumentException If the passed document is not MANAGED.
969
     *
970
     * @ignore
971
     */
972
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
973 19
    {
974
        // Ignore uninitialized proxy objects
975
        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...
976 19
            return;
977 1
        }
978
979
        $oid = spl_object_hash($document);
980 18
981
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
982 18
            throw new InvalidArgumentException('Document must be managed.');
983
        }
984
985
        if (! $class->isInheritanceTypeNone()) {
986 18
            $class = $this->dm->getClassMetadata(get_class($document));
987 2
        }
988
989
        $this->computeOrRecomputeChangeSet($class, $document, true);
990 18
    }
991 18
992
    /**
993
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
994
     */
995
    private function persistNew(ClassMetadata $class, object $document) : void
996 641
    {
997
        $this->lifecycleEventManager->prePersist($class, $document);
998 641
        $oid    = spl_object_hash($document);
999 641
        $upsert = false;
1000 641
        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...
1001 641
            $idValue = $class->getIdentifierValue($document);
1002 641
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1003 641
1004
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1005 641
                throw new InvalidArgumentException(sprintf(
1006 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1007 3
                    get_class($document)
1008 3
                ));
1009
            }
1010
1011
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1012 640
                throw new InvalidArgumentException(sprintf(
1013 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1014 1
                    get_class($document)
1015 1
                ));
1016
            }
1017
1018
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1019 639
                $idValue = $class->idGenerator->generate($this->dm, $document);
1020 560
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1021 560
                $class->setIdentifierValue($document, $idValue);
1022 560
            }
1023
1024
            $this->documentIdentifiers[$oid] = $idValue;
1025 639
        } else {
1026
            // this is for embedded documents without identifiers
1027
            $this->documentIdentifiers[$oid] = $oid;
1028 152
        }
1029
1030
        $this->documentStates[$oid] = self::STATE_MANAGED;
1031 639
1032
        if ($upsert) {
1033 639
            $this->scheduleForUpsert($class, $document);
1034 89
        } else {
1035
            $this->scheduleForInsert($class, $document);
1036 569
        }
1037
    }
1038 639
1039
    /**
1040
     * Executes all document insertions for documents of the specified type.
1041
     */
1042
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1043 530
    {
1044
        $persister = $this->getDocumentPersister($class->name);
1045 530
1046
        foreach ($documents as $oid => $document) {
1047 530
            $persister->addInsert($document);
1048 530
            unset($this->documentInsertions[$oid]);
1049 530
        }
1050
1051
        $persister->executeInserts($options);
1052 530
1053
        foreach ($documents as $document) {
1054 519
            $this->lifecycleEventManager->postPersist($class, $document);
1055 519
        }
1056
    }
1057 519
1058
    /**
1059
     * Executes all document upserts for documents of the specified type.
1060
     */
1061
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1062 86
    {
1063
        $persister = $this->getDocumentPersister($class->name);
1064 86
1065
        foreach ($documents as $oid => $document) {
1066 86
            $persister->addUpsert($document);
1067 86
            unset($this->documentUpserts[$oid]);
1068 86
        }
1069
1070
        $persister->executeUpserts($options);
1071 86
1072
        foreach ($documents as $document) {
1073 86
            $this->lifecycleEventManager->postPersist($class, $document);
1074 86
        }
1075
    }
1076 86
1077
    /**
1078
     * Executes all document updates for documents of the specified type.
1079
     */
1080
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1081 236
    {
1082
        if ($class->isReadOnly) {
1083 236
            return;
1084
        }
1085
1086
        $className = $class->name;
1087 236
        $persister = $this->getDocumentPersister($className);
1088 236
1089
        foreach ($documents as $oid => $document) {
1090 236
            $this->lifecycleEventManager->preUpdate($class, $document);
1091 236
1092
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1093 236
                $persister->update($document, $options);
1094 235
            }
1095
1096
            unset($this->documentUpdates[$oid]);
1097 229
1098
            $this->lifecycleEventManager->postUpdate($class, $document);
1099 229
        }
1100
    }
1101 228
1102
    /**
1103
     * Executes all document deletions for documents of the specified type.
1104
     */
1105
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1106 79
    {
1107
        $persister = $this->getDocumentPersister($class->name);
1108 79
1109
        foreach ($documents as $oid => $document) {
1110 79
            if (! $class->isEmbeddedDocument) {
1111 79
                $persister->delete($document, $options);
1112 36
            }
1113
            unset(
1114
                $this->documentDeletions[$oid],
1115 77
                $this->documentIdentifiers[$oid],
1116 77
                $this->originalDocumentData[$oid]
1117 77
            );
1118
1119
            // Clear snapshot information for any referenced PersistentCollection
1120
            // http://www.doctrine-project.org/jira/browse/MODM-95
1121
            foreach ($class->associationMappings as $fieldMapping) {
1122 77
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1123 53
                    continue;
1124 38
                }
1125
1126
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1127 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1128 33
                    continue;
1129 7
                }
1130
1131
                $value->clearSnapshot();
1132 29
            }
1133
1134
            // Document with this $oid after deletion treated as NEW, even if the $oid
1135
            // is obtained by a new document because the old one went out of scope.
1136
            $this->documentStates[$oid] = self::STATE_NEW;
1137 77
1138
            $this->lifecycleEventManager->postRemove($class, $document);
1139 77
        }
1140
    }
1141 77
1142
    /**
1143
     * Schedules a document for insertion into the database.
1144
     * If the document already has an identifier, it will be added to the
1145
     * identity map.
1146
     *
1147
     * @throws InvalidArgumentException
1148
     */
1149
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1150 572
    {
1151
        $oid = spl_object_hash($document);
1152 572
1153
        if (isset($this->documentUpdates[$oid])) {
1154 572
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1155
        }
1156
        if (isset($this->documentDeletions[$oid])) {
1157 572
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1158
        }
1159
        if (isset($this->documentInsertions[$oid])) {
1160 572
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1161
        }
1162
1163
        $this->documentInsertions[$oid] = $document;
1164 572
1165
        if (! isset($this->documentIdentifiers[$oid])) {
1166 572
            return;
1167 3
        }
1168
1169
        $this->addToIdentityMap($document);
1170 569
    }
1171 569
1172
    /**
1173
     * Schedules a document for upsert into the database and adds it to the
1174
     * identity map
1175
     *
1176
     * @throws InvalidArgumentException
1177
     */
1178
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1179 92
    {
1180
        $oid = spl_object_hash($document);
1181 92
1182
        if ($class->isEmbeddedDocument) {
1183 92
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1184
        }
1185
        if (isset($this->documentUpdates[$oid])) {
1186 92
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1187
        }
1188
        if (isset($this->documentDeletions[$oid])) {
1189 92
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1190
        }
1191
        if (isset($this->documentUpserts[$oid])) {
1192 92
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1193
        }
1194
1195
        $this->documentUpserts[$oid]     = $document;
1196 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1197 92
        $this->addToIdentityMap($document);
1198 92
    }
1199 92
1200
    /**
1201
     * Checks whether a document is scheduled for insertion.
1202
     */
1203
    public function isScheduledForInsert(object $document) : bool
1204 110
    {
1205
        return isset($this->documentInsertions[spl_object_hash($document)]);
1206 110
    }
1207
1208
    /**
1209
     * Checks whether a document is scheduled for upsert.
1210
     */
1211
    public function isScheduledForUpsert(object $document) : bool
1212 5
    {
1213
        return isset($this->documentUpserts[spl_object_hash($document)]);
1214 5
    }
1215
1216
    /**
1217
     * Schedules a document for being updated.
1218
     *
1219
     * @throws InvalidArgumentException
1220
     */
1221
    public function scheduleForUpdate(object $document) : void
1222 245
    {
1223
        $oid = spl_object_hash($document);
1224 245
        if (! isset($this->documentIdentifiers[$oid])) {
1225 245
            throw new InvalidArgumentException('Document has no identity.');
1226
        }
1227
1228
        if (isset($this->documentDeletions[$oid])) {
1229 245
            throw new InvalidArgumentException('Document is removed.');
1230
        }
1231
1232
        if (isset($this->documentUpdates[$oid])
1233 245
            || isset($this->documentInsertions[$oid])
1234 245
            || isset($this->documentUpserts[$oid])) {
1235 245
            return;
1236 104
        }
1237
1238
        $this->documentUpdates[$oid] = $document;
1239 243
    }
1240 243
1241
    /**
1242
     * Checks whether a document is registered as dirty in the unit of work.
1243
     * Note: Is not very useful currently as dirty documents are only registered
1244
     * at commit time.
1245
     */
1246
    public function isScheduledForUpdate(object $document) : bool
1247 21
    {
1248
        return isset($this->documentUpdates[spl_object_hash($document)]);
1249 21
    }
1250
1251
    /**
1252 1
     * Checks whether a document is registered to be checked in the unit of work.
1253
     */
1254 1
    public function isScheduledForDirtyCheck(object $document) : bool
1255 1
    {
1256
        $class = $this->dm->getClassMetadata(get_class($document));
1257
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1258
    }
1259
1260
    /**
1261
     * INTERNAL:
1262 84
     * Schedules a document for deletion.
1263
     */
1264 84
    public function scheduleForDelete(object $document) : void
1265
    {
1266 84
        $oid = spl_object_hash($document);
1267 2
1268 2
        if (isset($this->documentInsertions[$oid])) {
1269
            if ($this->isInIdentityMap($document)) {
1270 2
                $this->removeFromIdentityMap($document);
1271 2
            }
1272
            unset($this->documentInsertions[$oid]);
1273
            return; // document has not been persisted yet, so nothing more to do.
1274 83
        }
1275 2
1276
        if (! $this->isInIdentityMap($document)) {
1277
            return; // ignore
1278 82
        }
1279 82
1280
        $this->removeFromIdentityMap($document);
1281 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1282
1283
        if (isset($this->documentUpdates[$oid])) {
1284 82
            unset($this->documentUpdates[$oid]);
1285
        }
1286
        if (isset($this->documentDeletions[$oid])) {
1287
            return;
1288 82
        }
1289 82
1290
        $this->documentDeletions[$oid] = $document;
1291
    }
1292
1293
    /**
1294
     * Checks whether a document is registered as removed/deleted with the unit
1295 5
     * of work.
1296
     */
1297 5
    public function isScheduledForDelete(object $document) : bool
1298
    {
1299
        return isset($this->documentDeletions[spl_object_hash($document)]);
1300
    }
1301
1302
    /**
1303 257
     * Checks whether a document is scheduled for insertion, update or deletion.
1304
     */
1305 257
    public function isDocumentScheduled(object $document) : bool
1306 257
    {
1307 139
        $oid = spl_object_hash($document);
1308 129
        return isset($this->documentInsertions[$oid]) ||
1309 257
            isset($this->documentUpserts[$oid]) ||
1310
            isset($this->documentUpdates[$oid]) ||
1311
            isset($this->documentDeletions[$oid]);
1312
    }
1313
1314
    /**
1315
     * INTERNAL:
1316
     * Registers a document in the identity map.
1317
     *
1318
     * Note that documents in a hierarchy are registered with the class name of
1319
     * the root document. Identifiers are serialized before being used as array
1320
     * keys to allow differentiation of equal, but not identical, values.
1321
     *
1322 680
     * @ignore
1323
     */
1324 680
    public function addToIdentityMap(object $document) : bool
1325 680
    {
1326
        $class = $this->dm->getClassMetadata(get_class($document));
1327 680
        $id    = $this->getIdForIdentityMap($document);
1328 44
1329
        if (isset($this->identityMap[$class->name][$id])) {
1330
            return false;
1331 680
        }
1332
1333 680
        $this->identityMap[$class->name][$id] = $document;
1334 680
1335 3
        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...
1336
            ( ! $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...
1337
            $document->addPropertyChangedListener($this);
1338 680
        }
1339
1340
        return true;
1341
    }
1342
1343
    /**
1344
     * Gets the state of a document with regard to the current unit of work.
1345
     *
1346
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1347
     *                         This parameter can be set to improve performance of document state detection
1348
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1349 645
     *                         is either known or does not matter for the caller of the method.
1350
     */
1351 645
    public function getDocumentState(object $document, ?int $assume = null) : int
1352
    {
1353 645
        $oid = spl_object_hash($document);
1354 406
1355
        if (isset($this->documentStates[$oid])) {
1356
            return $this->documentStates[$oid];
1357 644
        }
1358
1359 644
        $class = $this->dm->getClassMetadata(get_class($document));
1360 192
1361
        if ($class->isEmbeddedDocument) {
1362
            return self::STATE_NEW;
1363 641
        }
1364 639
1365
        if ($assume !== null) {
1366
            return $assume;
1367
        }
1368
1369
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1370
         * known. Note that you cannot remember the NEW or DETACHED state in
1371
         * _documentStates since the UoW does not hold references to such
1372
         * objects and the object hash can be reused. More generally, because
1373
         * the state may "change" between NEW/DETACHED without the UoW being
1374 3
         * aware of it.
1375
         */
1376 3
        $id = $class->getIdentifierObject($document);
1377 2
1378
        if ($id === null) {
1379
            return self::STATE_NEW;
1380
        }
1381 2
1382
        // Check for a version field, if available, to avoid a DB lookup.
1383
        if ($class->isVersioned && $class->versionField !== null) {
1384
            return $class->getFieldValue($document, $class->versionField)
1385
                ? self::STATE_DETACHED
1386
                : self::STATE_NEW;
1387
        }
1388 2
1389 1
        // Last try before DB lookup: check the identity map.
1390
        if ($this->tryGetById($id, $class)) {
1391
            return self::STATE_DETACHED;
1392
        }
1393 2
1394 1
        // DB lookup
1395
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1396
            return self::STATE_DETACHED;
1397 1
        }
1398
1399
        return self::STATE_NEW;
1400
    }
1401
1402
    /**
1403
     * INTERNAL:
1404
     * Removes a document from the identity map. This effectively detaches the
1405
     * document from the persistence management of Doctrine.
1406
     *
1407
     * @throws InvalidArgumentException
1408
     *
1409 96
     * @ignore
1410
     */
1411 96
    public function removeFromIdentityMap(object $document) : bool
1412
    {
1413
        $oid = spl_object_hash($document);
1414 96
1415
        // Check if id is registered first
1416
        if (! isset($this->documentIdentifiers[$oid])) {
1417
            return false;
1418 96
        }
1419 96
1420
        $class = $this->dm->getClassMetadata(get_class($document));
1421 96
        $id    = $this->getIdForIdentityMap($document);
1422 96
1423 96
        if (isset($this->identityMap[$class->name][$id])) {
1424 96
            unset($this->identityMap[$class->name][$id]);
1425
            $this->documentStates[$oid] = self::STATE_DETACHED;
1426
            return true;
1427
        }
1428
1429
        return false;
1430
    }
1431
1432
    /**
1433
     * INTERNAL:
1434
     * Gets a document in the identity map by its identifier hash.
1435
     *
1436
     * @param mixed $id Document identifier
1437
     *
1438
     * @throws InvalidArgumentException If the class does not have an identifier.
1439
     *
1440 37
     * @ignore
1441
     */
1442 37
    public function getById($id, ClassMetadata $class) : object
1443
    {
1444
        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...
1445
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1446 37
        }
1447
1448 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1449
1450
        return $this->identityMap[$class->name][$serializedId];
1451
    }
1452
1453
    /**
1454
     * INTERNAL:
1455
     * Tries to get a document by its identifier hash. If no document is found
1456
     * for the given hash, FALSE is returned.
1457
     *
1458
     * @param mixed $id Document identifier
1459
     *
1460
     * @return mixed The found document or FALSE.
1461
     *
1462
     * @throws InvalidArgumentException If the class does not have an identifier.
1463
     *
1464 306
     * @ignore
1465
     */
1466 306
    public function tryGetById($id, ClassMetadata $class)
1467
    {
1468
        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...
1469
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1470 306
        }
1471
1472 306
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1473
1474
        return $this->identityMap[$class->name][$serializedId] ?? false;
1475
    }
1476
1477
    /**
1478
     * Schedules a document for dirty-checking at commit-time.
1479
     */
1480 3
    public function scheduleForSynchronization(object $document) : void
1481
    {
1482 3
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1483 3
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1484 3
    }
1485
1486
    /**
1487
     * Checks whether a document is registered in the identity map.
1488
     */
1489 92
    public function isInIdentityMap(object $document) : bool
1490
    {
1491 92
        $oid = spl_object_hash($document);
1492
1493 92
        if (! isset($this->documentIdentifiers[$oid])) {
1494 6
            return false;
1495
        }
1496
1497 90
        $class = $this->dm->getClassMetadata(get_class($document));
1498 90
        $id    = $this->getIdForIdentityMap($document);
1499
1500 90
        return isset($this->identityMap[$class->name][$id]);
1501
    }
1502
1503 680
    private function getIdForIdentityMap(object $document) : string
1504
    {
1505 680
        $class = $this->dm->getClassMetadata(get_class($document));
1506
1507 680
        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...
1508 158
            $id = spl_object_hash($document);
1509
        } else {
1510 679
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1511 679
            $id = serialize($class->getDatabaseIdentifierValue($id));
1512
        }
1513
1514 680
        return $id;
1515
    }
1516
1517
    /**
1518
     * INTERNAL:
1519
     * Checks whether an identifier exists in the identity map.
1520
     *
1521
     * @ignore
1522
     */
1523
    public function containsId($id, string $rootClassName) : bool
1524
    {
1525
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1526
    }
1527
1528
    /**
1529
     * Persists a document as part of the current unit of work.
1530
     *
1531
     * @throws MongoDBException If trying to persist MappedSuperclass.
1532
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1533
     */
1534 642
    public function persist(object $document) : void
1535
    {
1536 642
        $class = $this->dm->getClassMetadata(get_class($document));
1537 642
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1538 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1539
        }
1540 641
        $visited = [];
1541 641
        $this->doPersist($document, $visited);
1542 636
    }
1543
1544
    /**
1545
     * Saves a document as part of the current unit of work.
1546
     * This method is internally called during save() cascades as it tracks
1547
     * the already visited documents to prevent infinite recursions.
1548
     *
1549
     * NOTE: This method always considers documents that are not yet known to
1550
     * this UnitOfWork as NEW.
1551
     *
1552
     * @throws InvalidArgumentException
1553
     * @throws MongoDBException
1554
     */
1555 641
    private function doPersist(object $document, array &$visited) : void
1556
    {
1557 641
        $oid = spl_object_hash($document);
1558 641
        if (isset($visited[$oid])) {
1559 25
            return; // Prevent infinite recursion
1560
        }
1561
1562 641
        $visited[$oid] = $document; // Mark visited
1563
1564 641
        $class = $this->dm->getClassMetadata(get_class($document));
1565
1566 641
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1567
        switch ($documentState) {
1568 641
            case self::STATE_MANAGED:
1569
                // Nothing to do, except if policy is "deferred explicit"
1570 57
                if ($class->isChangeTrackingDeferredExplicit()) {
1571
                    $this->scheduleForSynchronization($document);
1572
                }
1573 57
                break;
1574 641
            case self::STATE_NEW:
1575 641
                if ($class->isFile) {
1576 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1577
                }
1578
1579 640
                $this->persistNew($class, $document);
1580 638
                break;
1581
1582 2
            case self::STATE_REMOVED:
1583
                // Document becomes managed again
1584 2
                unset($this->documentDeletions[$oid]);
1585
1586 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1587 2
                break;
1588
1589
            case self::STATE_DETACHED:
1590
                throw new InvalidArgumentException(
1591
                    'Behavior of persist() for a detached document is not yet defined.'
1592
                );
1593
1594
            default:
1595
                throw MongoDBException::invalidDocumentState($documentState);
1596
        }
1597
1598 638
        $this->cascadePersist($document, $visited);
1599 636
    }
1600
1601
    /**
1602
     * Deletes a document as part of the current unit of work.
1603
     */
1604 83
    public function remove(object $document)
1605
    {
1606 83
        $visited = [];
1607 83
        $this->doRemove($document, $visited);
1608 83
    }
1609
1610
    /**
1611
     * Deletes a document as part of the current unit of work.
1612
     *
1613
     * This method is internally called during delete() cascades as it tracks
1614
     * the already visited documents to prevent infinite recursions.
1615
     *
1616
     * @throws MongoDBException
1617
     */
1618 83
    private function doRemove(object $document, array &$visited) : void
1619
    {
1620 83
        $oid = spl_object_hash($document);
1621 83
        if (isset($visited[$oid])) {
1622 1
            return; // Prevent infinite recursion
1623
        }
1624
1625 83
        $visited[$oid] = $document; // mark visited
1626
1627
        /* Cascade first, because scheduleForDelete() removes the entity from
1628
         * the identity map, which can cause problems when a lazy Proxy has to
1629
         * be initialized for the cascade operation.
1630
         */
1631 83
        $this->cascadeRemove($document, $visited);
1632
1633 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1634 83
        $documentState = $this->getDocumentState($document);
1635
        switch ($documentState) {
1636 83
            case self::STATE_NEW:
1637 83
            case self::STATE_REMOVED:
1638
                // nothing to do
1639 1
                break;
1640 83
            case self::STATE_MANAGED:
1641 83
                $this->lifecycleEventManager->preRemove($class, $document);
1642 83
                $this->scheduleForDelete($document);
1643 83
                break;
1644
            case self::STATE_DETACHED:
1645
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1646
            default:
1647
                throw MongoDBException::invalidDocumentState($documentState);
1648
        }
1649 83
    }
1650
1651
    /**
1652
     * Merges the state of the given detached document into this UnitOfWork.
1653
     */
1654 11
    public function merge(object $document) : object
1655
    {
1656 11
        $visited = [];
1657
1658 11
        return $this->doMerge($document, $visited);
1659
    }
1660
1661
    /**
1662
     * Executes a merge operation on a document.
1663
     *
1664
     * @throws InvalidArgumentException If the entity instance is NEW.
1665
     * @throws LockException If the document uses optimistic locking through a
1666
     *                       version attribute and the version check against the
1667
     *                       managed copy fails.
1668
     */
1669 11
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1670
    {
1671 11
        $oid = spl_object_hash($document);
1672
1673 11
        if (isset($visited[$oid])) {
1674 1
            return $visited[$oid]; // Prevent infinite recursion
1675
        }
1676
1677 11
        $visited[$oid] = $document; // mark visited
1678
1679 11
        $class = $this->dm->getClassMetadata(get_class($document));
1680
1681
        /* First we assume DETACHED, although it can still be NEW but we can
1682
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1683
         * an identity, we need to fetch it from the DB anyway in order to
1684
         * merge. MANAGED documents are ignored by the merge operation.
1685
         */
1686 11
        $managedCopy = $document;
1687
1688 11
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1689 11
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1690
                $document->initializeProxy();
1691
            }
1692
1693 11
            $identifier = $class->getIdentifier();
1694
            // We always have one element in the identifier array but it might be null
1695 11
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1696 11
            $managedCopy = null;
1697
1698
            // Try to fetch document from the database
1699 11
            if (! $class->isEmbeddedDocument && $id !== null) {
1700 11
                $managedCopy = $this->dm->find($class->name, $id);
1701
1702
                // Managed copy may be removed in which case we can't merge
1703 11
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1704
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1705
                }
1706
1707 11
                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1708
                    $managedCopy->initializeProxy();
1709
                }
1710
            }
1711
1712 11
            if ($managedCopy === null) {
1713
                // Create a new managed instance
1714 4
                $managedCopy = $class->newInstance();
1715 4
                if ($id !== null) {
1716 3
                    $class->setIdentifierValue($managedCopy, $id);
1717
                }
1718 4
                $this->persistNew($class, $managedCopy);
1719
            }
1720
1721 11
            if ($class->isVersioned) {
1722
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1723
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1724
1725
                // Throw exception if versions don't match
1726
                if ($managedCopyVersion !== $documentVersion) {
1727
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1728
                }
1729
            }
1730
1731
            // Merge state of $document into existing (managed) document
1732 11
            foreach ($class->reflClass->getProperties() as $prop) {
1733 11
                $name = $prop->name;
1734 11
                $prop->setAccessible(true);
1735 11
                if (! isset($class->associationMappings[$name])) {
1736 11
                    if (! $class->isIdentifier($name)) {
1737 11
                        $prop->setValue($managedCopy, $prop->getValue($document));
1738
                    }
1739
                } else {
1740 11
                    $assoc2 = $class->associationMappings[$name];
1741
1742 11
                    if ($assoc2['type'] === 'one') {
1743 5
                        $other = $prop->getValue($document);
1744
1745 5
                        if ($other === null) {
1746 2
                            $prop->setValue($managedCopy, null);
1747 4
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1748
                            // Do not merge fields marked lazy that have not been fetched
1749
                            continue;
1750 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1751
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1752
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1753
                                /** @var ClassMetadata $targetClass */
1754
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1755
                                $relatedId   = $targetClass->getIdentifierObject($other);
1756
1757
                                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...
1758
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1759
                                } else {
1760
                                    $other = $this
1761
                                        ->dm
1762
                                        ->getProxyFactory()
1763
                                        ->getProxy($targetClass, $relatedId);
1764
                                    $this->registerManaged($other, $relatedId, []);
1765
                                }
1766
                            }
1767
1768 5
                            $prop->setValue($managedCopy, $other);
1769
                        }
1770
                    } else {
1771 10
                        $mergeCol = $prop->getValue($document);
1772
1773 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1774
                            /* Do not merge fields marked lazy that have not
1775
                             * been fetched. Keep the lazy persistent collection
1776
                             * of the managed copy.
1777
                             */
1778 3
                            continue;
1779
                        }
1780
1781 10
                        $managedCol = $prop->getValue($managedCopy);
1782
1783 10
                        if (! $managedCol) {
1784 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1785 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1786 1
                            $prop->setValue($managedCopy, $managedCol);
1787 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1788
                        }
1789
1790
                        /* Note: do not process association's target documents.
1791
                         * They will be handled during the cascade. Initialize
1792
                         * and, if necessary, clear $managedCol for now.
1793
                         */
1794 10
                        if ($assoc2['isCascadeMerge']) {
1795 10
                            $managedCol->initialize();
1796
1797
                            // If $managedCol differs from the merged collection, clear and set dirty
1798 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1799 3
                                $managedCol->unwrap()->clear();
1800 3
                                $managedCol->setDirty(true);
1801
1802 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1803
                                    $this->scheduleForSynchronization($managedCopy);
1804
                                }
1805
                            }
1806
                        }
1807
                    }
1808
                }
1809
1810 11
                if (! $class->isChangeTrackingNotify()) {
1811 11
                    continue;
1812
                }
1813
1814
                // Just treat all properties as changed, there is no other choice.
1815
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1816
            }
1817
1818 11
            if ($class->isChangeTrackingDeferredExplicit()) {
1819
                $this->scheduleForSynchronization($document);
1820
            }
1821
        }
1822
1823 11
        if ($prevManagedCopy !== null) {
1824 5
            $assocField = $assoc['fieldName'];
1825 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1826
1827 5
            if ($assoc['type'] === 'one') {
1828 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1829
            } else {
1830 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1831
1832 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1833 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1834
                }
1835
            }
1836
        }
1837
1838
        // Mark the managed copy visited as well
1839 11
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1840
1841 11
        $this->cascadeMerge($document, $managedCopy, $visited);
1842
1843 11
        return $managedCopy;
1844
    }
1845
1846
    /**
1847
     * Detaches a document from the persistence management. It's persistence will
1848
     * no longer be managed by Doctrine.
1849
     */
1850 11
    public function detach(object $document) : void
1851
    {
1852 11
        $visited = [];
1853 11
        $this->doDetach($document, $visited);
1854 11
    }
1855
1856
    /**
1857
     * Executes a detach operation on the given document.
1858
     *
1859
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1860
     */
1861 17
    private function doDetach(object $document, array &$visited) : void
1862
    {
1863 17
        $oid = spl_object_hash($document);
1864 17
        if (isset($visited[$oid])) {
1865 3
            return; // Prevent infinite recursion
1866
        }
1867
1868 17
        $visited[$oid] = $document; // mark visited
1869
1870 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1871 17
            case self::STATE_MANAGED:
1872 17
                $this->removeFromIdentityMap($document);
1873
                unset(
1874 17
                    $this->documentInsertions[$oid],
1875 17
                    $this->documentUpdates[$oid],
1876 17
                    $this->documentDeletions[$oid],
1877 17
                    $this->documentIdentifiers[$oid],
1878 17
                    $this->documentStates[$oid],
1879 17
                    $this->originalDocumentData[$oid],
1880 17
                    $this->parentAssociations[$oid],
1881 17
                    $this->documentUpserts[$oid],
1882 17
                    $this->hasScheduledCollections[$oid],
1883 17
                    $this->embeddedDocumentsRegistry[$oid]
1884
                );
1885 17
                break;
1886 3
            case self::STATE_NEW:
1887 3
            case self::STATE_DETACHED:
1888 3
                return;
1889
        }
1890
1891 17
        $this->cascadeDetach($document, $visited);
1892 17
    }
1893
1894
    /**
1895
     * Refreshes the state of the given document from the database, overwriting
1896
     * any local, unpersisted changes.
1897
     *
1898
     * @throws InvalidArgumentException If the document is not MANAGED.
1899
     */
1900 24
    public function refresh(object $document) : void
1901
    {
1902 24
        $visited = [];
1903 24
        $this->doRefresh($document, $visited);
1904 23
    }
1905
1906
    /**
1907
     * Executes a refresh operation on a document.
1908
     *
1909
     * @throws InvalidArgumentException If the document is not MANAGED.
1910
     */
1911 24
    private function doRefresh(object $document, array &$visited) : void
1912
    {
1913 24
        $oid = spl_object_hash($document);
1914 24
        if (isset($visited[$oid])) {
1915
            return; // Prevent infinite recursion
1916
        }
1917
1918 24
        $visited[$oid] = $document; // mark visited
1919
1920 24
        $class = $this->dm->getClassMetadata(get_class($document));
1921
1922 24
        if (! $class->isEmbeddedDocument) {
1923 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1924 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1925
            }
1926
1927 23
            $this->getDocumentPersister($class->name)->refresh($document);
1928
        }
1929
1930 23
        $this->cascadeRefresh($document, $visited);
1931 23
    }
1932
1933
    /**
1934
     * Cascades a refresh operation to associated documents.
1935
     */
1936 23
    private function cascadeRefresh(object $document, array &$visited) : void
1937
    {
1938 23
        $class = $this->dm->getClassMetadata(get_class($document));
1939
1940 23
        $associationMappings = array_filter(
1941 23
            $class->associationMappings,
1942
            static function ($assoc) {
1943 18
                return $assoc['isCascadeRefresh'];
1944 23
            }
1945
        );
1946
1947 23
        foreach ($associationMappings as $mapping) {
1948 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1949 15
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1950 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1951
                    // Unwrap so that foreach() does not initialize
1952 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1953
                }
1954 15
                foreach ($relatedDocuments as $relatedDocument) {
1955
                    $this->doRefresh($relatedDocument, $visited);
1956
                }
1957 10
            } elseif ($relatedDocuments !== null) {
1958 2
                $this->doRefresh($relatedDocuments, $visited);
1959
            }
1960
        }
1961 23
    }
1962
1963
    /**
1964
     * Cascades a detach operation to associated documents.
1965
     */
1966 17
    private function cascadeDetach(object $document, array &$visited) : void
1967
    {
1968 17
        $class = $this->dm->getClassMetadata(get_class($document));
1969 17
        foreach ($class->fieldMappings as $mapping) {
1970 17
            if (! $mapping['isCascadeDetach']) {
1971 17
                continue;
1972
            }
1973 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1974 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1975 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1976
                    // Unwrap so that foreach() does not initialize
1977 8
                    $relatedDocuments = $relatedDocuments->unwrap();
1978
                }
1979 11
                foreach ($relatedDocuments as $relatedDocument) {
1980 8
                    $this->doDetach($relatedDocument, $visited);
1981
                }
1982 11
            } elseif ($relatedDocuments !== null) {
1983 8
                $this->doDetach($relatedDocuments, $visited);
1984
            }
1985
        }
1986 17
    }
1987
    /**
1988
     * Cascades a merge operation to associated documents.
1989
     */
1990 11
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
1991
    {
1992 11
        $class = $this->dm->getClassMetadata(get_class($document));
1993
1994 11
        $associationMappings = array_filter(
1995 11
            $class->associationMappings,
1996
            static function ($assoc) {
1997 11
                return $assoc['isCascadeMerge'];
1998 11
            }
1999
        );
2000
2001 11
        foreach ($associationMappings as $assoc) {
2002 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2003
2004 11
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2005 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2006
                    // Collections are the same, so there is nothing to do
2007 1
                    continue;
2008
                }
2009
2010 8
                foreach ($relatedDocuments as $relatedDocument) {
2011 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2012
                }
2013 6
            } elseif ($relatedDocuments !== null) {
2014 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2015
            }
2016
        }
2017 11
    }
2018
2019
    /**
2020
     * Cascades the save operation to associated documents.
2021
     */
2022 638
    private function cascadePersist(object $document, array &$visited) : void
2023
    {
2024 638
        $class = $this->dm->getClassMetadata(get_class($document));
2025
2026 638
        $associationMappings = array_filter(
2027 638
            $class->associationMappings,
2028
            static function ($assoc) {
2029 493
                return $assoc['isCascadePersist'];
2030 638
            }
2031
        );
2032
2033 638
        foreach ($associationMappings as $fieldName => $mapping) {
2034 441
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2035
2036 441
            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...
2037 370
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2038 15
                    if ($relatedDocuments->getOwner() !== $document) {
2039 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2040
                    }
2041
                    // Unwrap so that foreach() does not initialize
2042 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2043
                }
2044
2045 370
                $count = 0;
2046 370
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2047 201
                    if (! empty($mapping['embedded'])) {
2048 130
                        [, $knownParent ] = $this->getParentAssociation($relatedDocument);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

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

Loading history...
2049 130
                        if ($knownParent && $knownParent !== $document) {
2050 1
                            $relatedDocument               = clone $relatedDocument;
2051 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2052
                        }
2053 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2054 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2055
                    }
2056 201
                    $this->doPersist($relatedDocument, $visited);
2057
                }
2058 344
            } elseif ($relatedDocuments !== null) {
2059 129
                if (! empty($mapping['embedded'])) {
2060 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2061 69
                    if ($knownParent && $knownParent !== $document) {
2062 3
                        $relatedDocuments = clone $relatedDocuments;
2063 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2064
                    }
2065 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2066
                }
2067 129
                $this->doPersist($relatedDocuments, $visited);
2068
            }
2069
        }
2070 636
    }
2071
2072
    /**
2073
     * Cascades the delete operation to associated documents.
2074
     */
2075 83
    private function cascadeRemove(object $document, array &$visited) : void
2076
    {
2077 83
        $class = $this->dm->getClassMetadata(get_class($document));
2078 83
        foreach ($class->fieldMappings as $mapping) {
2079 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2080 82
                continue;
2081
            }
2082 43
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2083 3
                $document->initializeProxy();
2084
            }
2085
2086 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2087 43
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2088
                // If its a PersistentCollection initialization is intended! No unwrap!
2089 31
                foreach ($relatedDocuments as $relatedDocument) {
2090 15
                    $this->doRemove($relatedDocument, $visited);
2091
                }
2092 27
            } elseif ($relatedDocuments !== null) {
2093 14
                $this->doRemove($relatedDocuments, $visited);
2094
            }
2095
        }
2096 83
    }
2097
2098
    /**
2099
     * Acquire a lock on the given document.
2100
     *
2101
     * @throws LockException
2102
     * @throws InvalidArgumentException
2103
     */
2104 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2105
    {
2106 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2107 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2108
        }
2109
2110 7
        $documentName = get_class($document);
2111 7
        $class        = $this->dm->getClassMetadata($documentName);
2112
2113 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2114 2
            if (! $class->isVersioned) {
2115 1
                throw LockException::notVersioned($documentName);
2116
            }
2117
2118 1
            if ($lockVersion !== null) {
2119 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2120 1
                if ($documentVersion !== $lockVersion) {
2121 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2122
                }
2123
            }
2124 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2125 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2126
        }
2127 5
    }
2128
2129
    /**
2130
     * Releases a lock on the given document.
2131
     *
2132
     * @throws InvalidArgumentException
2133
     */
2134 1
    public function unlock(object $document) : void
2135
    {
2136 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2137
            throw new InvalidArgumentException('Document is not MANAGED.');
2138
        }
2139 1
        $documentName = get_class($document);
2140 1
        $this->getDocumentPersister($documentName)->unlock($document);
2141 1
    }
2142
2143
    /**
2144
     * Clears the UnitOfWork.
2145
     */
2146 378
    public function clear(?string $documentName = null) : void
2147
    {
2148 378
        if ($documentName === null) {
2149 372
            $this->identityMap                 =
2150 372
            $this->documentIdentifiers         =
2151 372
            $this->originalDocumentData        =
2152 372
            $this->documentChangeSets          =
2153 372
            $this->documentStates              =
2154 372
            $this->scheduledForSynchronization =
2155 372
            $this->documentInsertions          =
2156 372
            $this->documentUpserts             =
2157 372
            $this->documentUpdates             =
2158 372
            $this->documentDeletions           =
2159 372
            $this->collectionUpdates           =
2160 372
            $this->collectionDeletions         =
2161 372
            $this->parentAssociations          =
2162 372
            $this->embeddedDocumentsRegistry   =
2163 372
            $this->orphanRemovals              =
2164 372
            $this->hasScheduledCollections     = [];
2165
        } else {
2166 6
            $visited = [];
2167 6
            foreach ($this->identityMap as $className => $documents) {
2168 6
                if ($className !== $documentName) {
2169 3
                    continue;
2170
                }
2171
2172 6
                foreach ($documents as $document) {
2173 6
                    $this->doDetach($document, $visited);
2174
                }
2175
            }
2176
        }
2177
2178 378
        if (! $this->evm->hasListeners(Events::onClear)) {
2179 378
            return;
2180
        }
2181
2182
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2183
    }
2184
2185
    /**
2186
     * INTERNAL:
2187
     * Schedules an embedded document for removal. The remove() operation will be
2188
     * invoked on that document at the beginning of the next commit of this
2189
     * UnitOfWork.
2190
     *
2191
     * @ignore
2192
     */
2193 58
    public function scheduleOrphanRemoval(object $document) : void
2194
    {
2195 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2196 58
    }
2197
2198
    /**
2199
     * INTERNAL:
2200
     * Unschedules an embedded or referenced object for removal.
2201
     *
2202
     * @ignore
2203
     */
2204 123
    public function unscheduleOrphanRemoval(object $document) : void
2205
    {
2206 123
        $oid = spl_object_hash($document);
2207 123
        unset($this->orphanRemovals[$oid]);
2208 123
    }
2209
2210
    /**
2211
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2212
     *  1) sets owner if it was cloned
2213
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2214
     *  3) NOP if state is OK
2215
     * Returned collection should be used from now on (only important with 2nd point)
2216
     */
2217 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2218
    {
2219 8
        $owner = $coll->getOwner();
2220 8
        if ($owner === null) { // cloned
2221 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2222 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2223 2
            if (! $coll->isInitialized()) {
2224 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2225
            }
2226 2
            $newValue = clone $coll;
2227 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2228 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2229 2
            if ($this->isScheduledForUpdate($document)) {
2230
                // @todo following line should be superfluous once collections are stored in change sets
2231
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2232
            }
2233 2
            return $newValue;
2234
        }
2235 6
        return $coll;
2236
    }
2237
2238
    /**
2239
     * INTERNAL:
2240
     * Schedules a complete collection for removal when this UnitOfWork commits.
2241
     */
2242 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2243
    {
2244 47
        $oid = spl_object_hash($coll);
2245 47
        unset($this->collectionUpdates[$oid]);
2246 47
        if (isset($this->collectionDeletions[$oid])) {
2247
            return;
2248
        }
2249
2250 47
        $this->collectionDeletions[$oid] = $coll;
2251 47
        $this->scheduleCollectionOwner($coll);
2252 47
    }
2253
2254
    /**
2255
     * Checks whether a PersistentCollection is scheduled for deletion.
2256
     */
2257 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2258
    {
2259 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2260
    }
2261
2262
    /**
2263
     * INTERNAL:
2264
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2265
     */
2266 227
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2267
    {
2268 227
        if ($coll->getOwner() === null) {
2269
            return;
2270
        }
2271
2272 227
        $oid = spl_object_hash($coll);
2273 227
        if (! isset($this->collectionDeletions[$oid])) {
2274 227
            return;
2275
        }
2276
2277 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2278 14
        unset($this->collectionDeletions[$oid]);
2279 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2280 14
    }
2281
2282
    /**
2283
     * INTERNAL:
2284
     * Schedules a collection for update when this UnitOfWork commits.
2285
     */
2286 254
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2287
    {
2288 254
        $mapping = $coll->getMapping();
2289 254
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2290
            /* There is no need to $unset collection if it will be $set later
2291
             * This is NOP if collection is not scheduled for deletion
2292
             */
2293 44
            $this->unscheduleCollectionDeletion($coll);
2294
        }
2295 254
        $oid = spl_object_hash($coll);
2296 254
        if (isset($this->collectionUpdates[$oid])) {
2297 11
            return;
2298
        }
2299
2300 254
        $this->collectionUpdates[$oid] = $coll;
2301 254
        $this->scheduleCollectionOwner($coll);
2302 254
    }
2303
2304
    /**
2305
     * INTERNAL:
2306
     * Unschedules a collection from being updated when this UnitOfWork commits.
2307
     */
2308 227
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2309
    {
2310 227
        if ($coll->getOwner() === null) {
2311
            return;
2312
        }
2313
2314 227
        $oid = spl_object_hash($coll);
2315 227
        if (! isset($this->collectionUpdates[$oid])) {
2316 52
            return;
2317
        }
2318
2319 216
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2320 216
        unset($this->collectionUpdates[$oid]);
2321 216
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2322 216
    }
2323
2324
    /**
2325
     * Checks whether a PersistentCollection is scheduled for update.
2326
     */
2327 140
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2328
    {
2329 140
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2330
    }
2331
2332
    /**
2333
     * INTERNAL:
2334
     * Gets PersistentCollections that have been visited during computing change
2335
     * set of $document
2336
     *
2337
     * @return PersistentCollectionInterface[]
2338
     */
2339 581
    public function getVisitedCollections(object $document) : array
2340
    {
2341 581
        $oid = spl_object_hash($document);
2342
2343 581
        return $this->visitedCollections[$oid] ?? [];
2344
    }
2345
2346
    /**
2347
     * INTERNAL:
2348
     * Gets PersistentCollections that are scheduled to update and related to $document
2349
     *
2350
     * @return PersistentCollectionInterface[]
2351
     */
2352 581
    public function getScheduledCollections(object $document) : array
2353
    {
2354 581
        $oid = spl_object_hash($document);
2355
2356 581
        return $this->hasScheduledCollections[$oid] ?? [];
2357
    }
2358
2359
    /**
2360
     * Checks whether the document is related to a PersistentCollection
2361
     * scheduled for update or deletion.
2362
     */
2363 56
    public function hasScheduledCollections(object $document) : bool
2364
    {
2365 56
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2366
    }
2367
2368
    /**
2369
     * Marks the PersistentCollection's top-level owner as having a relation to
2370
     * a collection scheduled for update or deletion.
2371
     *
2372
     * If the owner is not scheduled for any lifecycle action, it will be
2373
     * scheduled for update to ensure that versioning takes place if necessary.
2374
     *
2375
     * If the collection is nested within atomic collection, it is immediately
2376
     * unscheduled and atomic one is scheduled for update instead. This makes
2377
     * calculating update data way easier.
2378
     */
2379 256
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2380
    {
2381 256
        if ($coll->getOwner() === null) {
2382
            return;
2383
        }
2384
2385 256
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2386 256
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2387
2388 256
        if ($document !== $coll->getOwner()) {
2389 26
            $parent  = $coll->getOwner();
2390 26
            $mapping = [];
2391 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2392 26
                [$mapping, $parent ] = $parentAssoc;
2393
            }
2394 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2395 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2396 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2397 8
                $this->scheduleCollectionUpdate($atomicCollection);
2398 8
                $this->unscheduleCollectionDeletion($coll);
2399 8
                $this->unscheduleCollectionUpdate($coll);
2400
            }
2401
        }
2402
2403 256
        if ($this->isDocumentScheduled($document)) {
2404 251
            return;
2405
        }
2406
2407 55
        $this->scheduleForUpdate($document);
2408 55
    }
2409
2410
    /**
2411
     * Get the top-most owning document of a given document
2412
     *
2413
     * If a top-level document is provided, that same document will be returned.
2414
     * For an embedded document, we will walk through parent associations until
2415
     * we find a top-level document.
2416
     *
2417
     * @throws UnexpectedValueException When a top-level document could not be found.
2418
     */
2419 258
    public function getOwningDocument(object $document) : object
2420
    {
2421 258
        $class = $this->dm->getClassMetadata(get_class($document));
2422 258
        while ($class->isEmbeddedDocument) {
2423 42
            $parentAssociation = $this->getParentAssociation($document);
2424
2425 42
            if (! $parentAssociation) {
2426
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2427
            }
2428
2429 42
            [, $document ] = $parentAssociation;
2430 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2431
        }
2432
2433 258
        return $document;
2434
    }
2435
2436
    /**
2437
     * Gets the class name for an association (embed or reference) with respect
2438
     * to any discriminator value.
2439
     *
2440
     * @param array|null $data
2441
     */
2442 227
    public function getClassNameForAssociation(array $mapping, $data) : string
2443
    {
2444 227
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2445
2446 227
        $discriminatorValue = null;
2447 227
        if (isset($discriminatorField, $data[$discriminatorField])) {
2448 13
            $discriminatorValue = $data[$discriminatorField];
2449 215
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2450
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2451
        }
2452
2453 227
        if ($discriminatorValue !== null) {
2454 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2455 13
                ?? (string) $discriminatorValue;
2456
        }
2457
2458 215
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2459
2460 215
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2461 11
            $discriminatorValue = $data[$class->discriminatorField];
2462 205
        } elseif ($class->defaultDiscriminatorValue !== null) {
2463 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2464
        }
2465
2466 215
        if ($discriminatorValue !== null) {
2467 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2468
        }
2469
2470 204
        return $mapping['targetDocument'];
2471
    }
2472
2473
    /**
2474
     * INTERNAL:
2475
     * Creates a document. Used for reconstitution of documents during hydration.
2476
     *
2477
     * @internal Highly performance-sensitive method.
2478
     *
2479
     * @ignore
2480
     */
2481 402
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2482
    {
2483 402
        $class = $this->dm->getClassMetadata($className);
2484
2485
        // @TODO figure out how to remove this
2486 402
        $discriminatorValue = null;
2487 402
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2488 15
            $discriminatorValue = $data[$class->discriminatorField];
2489 394
        } elseif (isset($class->defaultDiscriminatorValue)) {
2490 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2491
        }
2492
2493 402
        if ($discriminatorValue !== null) {
2494 16
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2495
2496 16
            $class = $this->dm->getClassMetadata($className);
2497
2498 16
            unset($data[$class->discriminatorField]);
2499
        }
2500
2501 402
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2502 2
            $document = $class->newInstance();
2503 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2504 2
            return $document;
2505
        }
2506
2507 401
        $isManagedObject = false;
2508 401
        $serializedId    = null;
2509 401
        $id              = null;
2510 401
        if (! $class->isQueryResultDocument) {
2511 398
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2512 398
            $serializedId    = serialize($id);
2513 398
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2514
        }
2515
2516 401
        $oid = null;
2517 401
        if ($isManagedObject) {
2518 101
            $document = $this->identityMap[$class->name][$serializedId];
2519 101
            $oid      = spl_object_hash($document);
2520 101
            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...
2521 16
                $document->setProxyInitializer(null);
2522 16
                $overrideLocalValues = true;
2523 16
                if ($document instanceof NotifyPropertyChanged) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\NotifyPropertyChanged does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2524 16
                    $document->addPropertyChangedListener($this);
2525
                }
2526
            } else {
2527 91
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2528
            }
2529 101
            if ($overrideLocalValues) {
2530 42
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2531 101
                $this->originalDocumentData[$oid] = $data;
2532
            }
2533
        } else {
2534 352
            if ($document === null) {
2535 352
                $document = $class->newInstance();
2536
            }
2537
2538 352
            if (! $class->isQueryResultDocument) {
2539 348
                $this->registerManaged($document, $id, $data);
2540 348
                $oid                                            = spl_object_hash($document);
2541 348
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2542 348
                $this->identityMap[$class->name][$serializedId] = $document;
2543
            }
2544
2545 352
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2546
2547 352
            if (! $class->isQueryResultDocument) {
2548 348
                $this->originalDocumentData[$oid] = $data;
2549
            }
2550
        }
2551
2552 401
        return $document;
2553
    }
2554
2555
    /**
2556
     * Initializes (loads) an uninitialized persistent collection of a document.
2557
     */
2558 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
2559
    {
2560 178
        if ($collection->getOwner() === null) {
2561
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2562
        }
2563
2564 178
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2565 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2566 178
    }
2567
2568
    /**
2569
     * Gets the identity map of the UnitOfWork.
2570
     */
2571
    public function getIdentityMap() : array
2572
    {
2573
        return $this->identityMap;
2574
    }
2575
2576
    /**
2577
     * Gets the original data of a document. The original data is the data that was
2578
     * present at the time the document was reconstituted from the database.
2579
     *
2580
     * @return array
2581
     */
2582 1
    public function getOriginalDocumentData(object $document) : array
2583
    {
2584 1
        $oid = spl_object_hash($document);
2585
2586 1
        return $this->originalDocumentData[$oid] ?? [];
2587
    }
2588
2589 60
    public function setOriginalDocumentData(object $document, array $data) : void
2590
    {
2591 60
        $oid                              = spl_object_hash($document);
2592 60
        $this->originalDocumentData[$oid] = $data;
2593 60
        unset($this->documentChangeSets[$oid]);
2594 60
    }
2595
2596
    /**
2597
     * INTERNAL:
2598
     * Sets a property value of the original data array of a document.
2599
     *
2600
     * @param mixed $value
2601
     *
2602
     * @ignore
2603
     */
2604 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2605
    {
2606 3
        $this->originalDocumentData[$oid][$property] = $value;
2607 3
    }
2608
2609
    /**
2610
     * Gets the identifier of a document.
2611
     *
2612
     * @return mixed The identifier value
2613
     */
2614 452
    public function getDocumentIdentifier(object $document)
2615
    {
2616 452
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2617
    }
2618
2619
    /**
2620
     * Checks whether the UnitOfWork has any pending insertions.
2621
     *
2622
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2623
     */
2624
    public function hasPendingInsertions() : bool
2625
    {
2626
        return ! empty($this->documentInsertions);
2627
    }
2628
2629
    /**
2630
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2631
     * number of documents in the identity map.
2632
     */
2633 2
    public function size() : int
2634
    {
2635 2
        $count = 0;
2636 2
        foreach ($this->identityMap as $documentSet) {
2637 2
            $count += count($documentSet);
2638
        }
2639 2
        return $count;
2640
    }
2641
2642
    /**
2643
     * INTERNAL:
2644
     * Registers a document as managed.
2645
     *
2646
     * TODO: This method assumes that $id is a valid PHP identifier for the
2647
     * document class. If the class expects its database identifier to be an
2648
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2649
     * document identifiers map will become inconsistent with the identity map.
2650
     * In the future, we may want to round-trip $id through a PHP and database
2651
     * conversion and throw an exception if it's inconsistent.
2652
     *
2653
     * @param mixed $id The identifier values.
2654
     */
2655 381
    public function registerManaged(object $document, $id, array $data) : void
2656
    {
2657 381
        $oid   = spl_object_hash($document);
2658 381
        $class = $this->dm->getClassMetadata(get_class($document));
2659
2660 381
        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...
2661 110
            $this->documentIdentifiers[$oid] = $oid;
2662
        } else {
2663 375
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2664
        }
2665
2666 381
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2667 381
        $this->originalDocumentData[$oid] = $data;
2668 381
        $this->addToIdentityMap($document);
2669 381
    }
2670
2671
    /**
2672
     * INTERNAL:
2673
     * Clears the property changeset of the document with the given OID.
2674
     */
2675
    public function clearDocumentChangeSet(string $oid)
2676
    {
2677
        $this->documentChangeSets[$oid] = [];
2678
    }
2679
2680
    /* PropertyChangedListener implementation */
2681
2682
    /**
2683
     * Notifies this UnitOfWork of a property change in a document.
2684
     *
2685
     * @param object $document     The document that owns the property.
2686
     * @param string $propertyName The name of the property that changed.
2687
     * @param mixed  $oldValue     The old value of the property.
2688
     * @param mixed  $newValue     The new value of the property.
2689
     */
2690 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2691
    {
2692 2
        $oid   = spl_object_hash($document);
2693 2
        $class = $this->dm->getClassMetadata(get_class($document));
2694
2695 2
        if (! isset($class->fieldMappings[$propertyName])) {
2696 1
            return; // ignore non-persistent fields
2697
        }
2698
2699
        // Update changeset and mark document for synchronization
2700 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2701 2
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2702
            return;
2703
        }
2704
2705 2
        $this->scheduleForSynchronization($document);
2706 2
    }
2707
2708
    /**
2709
     * Gets the currently scheduled document insertions in this UnitOfWork.
2710
     */
2711 3
    public function getScheduledDocumentInsertions() : array
2712
    {
2713 3
        return $this->documentInsertions;
2714
    }
2715
2716
    /**
2717
     * Gets the currently scheduled document upserts in this UnitOfWork.
2718
     */
2719 1
    public function getScheduledDocumentUpserts() : array
2720
    {
2721 1
        return $this->documentUpserts;
2722
    }
2723
2724
    /**
2725
     * Gets the currently scheduled document updates in this UnitOfWork.
2726
     */
2727 2
    public function getScheduledDocumentUpdates() : array
2728
    {
2729 2
        return $this->documentUpdates;
2730
    }
2731
2732
    /**
2733
     * Gets the currently scheduled document deletions in this UnitOfWork.
2734
     */
2735
    public function getScheduledDocumentDeletions() : array
2736
    {
2737
        return $this->documentDeletions;
2738
    }
2739
2740
    /**
2741
     * Get the currently scheduled complete collection deletions
2742
     */
2743
    public function getScheduledCollectionDeletions() : array
2744
    {
2745
        return $this->collectionDeletions;
2746
    }
2747
2748
    /**
2749
     * Gets the currently scheduled collection inserts, updates and deletes.
2750
     */
2751
    public function getScheduledCollectionUpdates() : array
2752
    {
2753
        return $this->collectionUpdates;
2754
    }
2755
2756
    /**
2757
     * Helper method to initialize a lazy loading proxy or persistent collection.
2758
     */
2759
    public function initializeObject(object $obj) : void
2760
    {
2761
        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...
2762
            $obj->initializeProxy();
2763
        } elseif ($obj instanceof PersistentCollectionInterface) {
2764
            $obj->initialize();
2765
        }
2766
    }
2767
2768
    private function objToStr(object $obj) : string
2769
    {
2770
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2771
    }
2772
}
2773