Completed
Pull Request — master (#2166)
by Andreas
18:35
created

UnitOfWork::isDocumentScheduled()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

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

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

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

Loading history...
377 259
            $this->documentUpserts ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
378 217
            $this->documentDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
379 200
            $this->documentUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->documentUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
380 22
            $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
381 22
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
382 617
            $this->orphanRemovals)
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
383
        ) {
384 22
            return; // Nothing to do.
385
        }
386
387 614
        $this->commitsInProgress++;
388 614
        if ($this->commitsInProgress > 1) {
389
            throw MongoDBException::commitInProgress();
390
        }
391
        try {
392 614
            if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
393 55
                foreach ($this->orphanRemovals as $removal) {
394 55
                    $this->remove($removal);
395
                }
396
            }
397
398
            // Raise onFlush
399 614
            if ($this->evm->hasListeners(Events::onFlush)) {
400 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
401
            }
402
403 613
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
404 86
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $class does not exist. Did you forget to declare it?

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

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

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

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

Loading history...
405 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...
406
            }
407
408 613
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
409 537
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
421
            }
422
423
            // Raise postFlush
424 600
            if ($this->evm->hasListeners(Events::postFlush)) {
425
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
426
            }
427
428
            // Clear up
429 600
            $this->documentInsertions          =
430 600
            $this->documentUpserts             =
431 600
            $this->documentUpdates             =
432 600
            $this->documentDeletions           =
433 600
            $this->documentChangeSets          =
434 600
            $this->collectionUpdates           =
435 600
            $this->collectionDeletions         =
436 600
            $this->visitedCollections          =
437 600
            $this->scheduledForSynchronization =
438 600
            $this->orphanRemovals              =
439 600
            $this->hasScheduledCollections     = [];
440 600
        } finally {
441 614
            $this->commitsInProgress--;
442
        }
443 600
    }
444
445
    /**
446
     * Groups a list of scheduled documents by their class.
447
     */
448 613
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
449
    {
450 613
        if (empty($documents)) {
451 613
            return [];
452
        }
453 612
        $divided = [];
454 612
        $embeds  = [];
455 612
        foreach ($documents as $oid => $d) {
456 612
            $className = get_class($d);
457 612
            if (isset($embeds[$className])) {
458 81
                continue;
459
            }
460 612
            if (isset($divided[$className])) {
461 167
                $divided[$className][1][$oid] = $d;
462 167
                continue;
463
            }
464 612
            $class = $this->dm->getClassMetadata($className);
465 612
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
466 183
                $embeds[$className] = true;
467 183
                continue;
468
            }
469 612
            if (empty($divided[$class->name])) {
470 612
                $divided[$class->name] = [$class, [$oid => $d]];
471
            } else {
472 4
                $divided[$class->name][1][$oid] = $d;
473
            }
474
        }
475
476 612
        return $divided;
477
    }
478
479
    /**
480
     * Compute changesets of all documents scheduled for insertion.
481
     *
482
     * Embedded documents will not be processed.
483
     */
484 623
    private function computeScheduleInsertsChangeSets() : void
485
    {
486 623
        foreach ($this->documentInsertions as $document) {
487 550
            $class = $this->dm->getClassMetadata(get_class($document));
488 550
            if ($class->isEmbeddedDocument) {
489 165
                continue;
490
            }
491
492 544
            $this->computeChangeSet($class, $document);
493
        }
494 622
    }
495
496
    /**
497
     * Compute changesets of all documents scheduled for upsert.
498
     *
499
     * Embedded documents will not be processed.
500
     */
501 622
    private function computeScheduleUpsertsChangeSets() : void
502
    {
503 622
        foreach ($this->documentUpserts as $document) {
504 85
            $class = $this->dm->getClassMetadata(get_class($document));
505 85
            if ($class->isEmbeddedDocument) {
506
                continue;
507
            }
508
509 85
            $this->computeChangeSet($class, $document);
510
        }
511 622
    }
512
513
    /**
514
     * Gets the changeset for a document.
515
     *
516
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
517
     */
518 628
    public function getDocumentChangeSet(object $document) : array
519
    {
520 628
        $oid = spl_object_hash($document);
521
522 628
        return $this->documentChangeSets[$oid] ?? [];
523
    }
524
525
    /**
526
     * Sets the changeset for a document.
527
     *
528
     * @internal
529
     */
530 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
531
    {
532 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
533 1
    }
534
535
    /**
536
     * Get a documents actual data, flattening all the objects to arrays.
537
     *
538
     * @internal
539
     *
540
     * @return array
541
     */
542 631
    public function getDocumentActualData(object $document) : array
543
    {
544 631
        $class      = $this->dm->getClassMetadata(get_class($document));
545 631
        $actualData = [];
546 631
        foreach ($class->reflFields as $name => $refProp) {
547 631
            $mapping = $class->fieldMappings[$name];
548
            // skip not saved fields
549 631
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
550 202
                continue;
551
            }
552 631
            $value = $refProp->getValue($document);
553 631
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
554 631
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
555
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
556 406
                if (! $value instanceof Collection) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
741
742 37
                    if ($orgTimestamp === $actualTimestamp) {
743 30
                        continue;
744
                    }
745
                }
746
747
                // regular field
748 134
                $changeSet[$propName] = [$orgValue, $actualValue];
749
            }
750 297
            if ($changeSet) {
751 245
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
752 18
                    ? $changeSet + $this->documentChangeSets[$oid]
753 242
                    : $changeSet;
754
755 245
                $this->originalDocumentData[$oid] = $actualData;
756 245
                $this->scheduleForUpdate($document);
757
            }
758
        }
759
760
        // Look for changes in associations of the document
761 627
        $associationMappings = array_filter(
762 627
            $class->associationMappings,
763
            static function ($assoc) {
764 484
                return empty($assoc['notSaved']);
765 627
            }
766
        );
767
768 627
        foreach ($associationMappings as $mapping) {
769 484
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
770
771 484
            if ($value === null) {
772 326
                continue;
773
            }
774
775 466
            $this->computeAssociationChanges($document, $mapping, $value);
776
777 465
            if (isset($mapping['reference'])) {
778 348
                continue;
779
            }
780
781 359
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
782
783 359
            foreach ($values as $obj) {
784 188
                $oid2 = spl_object_hash($obj);
785
786 188
                if (isset($this->documentChangeSets[$oid2])) {
787 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
788
                        // instance of $value is the same as it was previously otherwise there would be
789
                        // change set already in place
790 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
791
                    }
792
793 186
                    if (! $isNewDocument) {
794 87
                        $this->scheduleForUpdate($document);
795
                    }
796
797 186
                    break;
798
                }
799
            }
800
        }
801 626
    }
802
803
    /**
804
     * Computes all the changes that have been done to documents and collections
805
     * since the last commit and stores these changes in the _documentChangeSet map
806
     * temporarily for access by the persisters, until the UoW commit is finished.
807
     */
808 623
    public function computeChangeSets() : void
809
    {
810 623
        $this->computeScheduleInsertsChangeSets();
811 622
        $this->computeScheduleUpsertsChangeSets();
812
813
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
814 622
        foreach ($this->identityMap as $className => $documents) {
815 622
            $class = $this->dm->getClassMetadata($className);
816 622
            if ($class->isEmbeddedDocument) {
817
                /* we do not want to compute changes to embedded documents up front
818
                 * in case embedded document was replaced and its changeset
819
                 * would corrupt data. Embedded documents' change set will
820
                 * be calculated by reachability from owning document.
821
                 */
822 178
                continue;
823
            }
824
825
            // If change tracking is explicit or happens through notification, then only compute
826
            // changes on document of that type that are explicitly marked for synchronization.
827
            switch (true) {
828 622
                case $class->isChangeTrackingDeferredImplicit():
829 621
                    $documentsToProcess = $documents;
830 621
                    break;
831
832 3
                case isset($this->scheduledForSynchronization[$className]):
833 2
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
834 2
                    break;
835
836
                default:
837 3
                    $documentsToProcess = [];
838
            }
839
840 622
            foreach ($documentsToProcess as $document) {
841
                // Ignore uninitialized proxy objects
842 617
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
843 10
                    continue;
844
                }
845
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
846 617
                $oid = spl_object_hash($document);
847 617
                if (isset($this->documentInsertions[$oid])
848 340
                    || isset($this->documentUpserts[$oid])
849 294
                    || isset($this->documentDeletions[$oid])
850 617
                    || ! isset($this->documentStates[$oid])
851
                ) {
852 615
                    continue;
853
                }
854
855 294
                $this->computeChangeSet($class, $document);
856
            }
857
        }
858 622
    }
859
860
    /**
861
     * Computes the changes of an association.
862
     *
863
     * @param mixed $value The value of the association.
864
     *
865
     * @throws InvalidArgumentException
866
     */
867 466
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
868
    {
869 466
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
870 466
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
871 466
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
872
873 466
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
874 7
            return;
875
        }
876
877 465
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
878 257
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
879 253
                $this->scheduleCollectionUpdate($value);
880
            }
881
882 257
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
883 257
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
884 257
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
885 151
                $value->initialize();
886 151
                foreach ($value->getDeletedDocuments() as $orphan) {
887 25
                    $this->scheduleOrphanRemoval($orphan);
888
                }
889
            }
890
        }
891
892
        // Look through the documents, and in any of their associations,
893
        // for transient (new) documents, recursively. ("Persistence by reachability")
894
        // Unwrap. Uninitialized collections will simply be empty.
895 465
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
896
897 465
        $count = 0;
898 465
        foreach ($unwrappedValue as $key => $entry) {
899 375
            if (! is_object($entry)) {
900 1
                throw new InvalidArgumentException(
901 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
902
                );
903
            }
904
905 374
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
906
907 374
            $state = $this->getDocumentState($entry, self::STATE_NEW);
908
909
            // Handle "set" strategy for multi-level hierarchy
910 374
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
911 374
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
912
913 374
            $count++;
914
915
            switch ($state) {
916 374
                case self::STATE_NEW:
917 70
                    if (! $assoc['isCascadePersist']) {
918
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
919
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
920
                            . ' Explicitly persist the new document or configure cascading persist operations'
921
                            . ' on the relationship.');
922
                    }
923
924 70
                    $this->persistNew($targetClass, $entry);
925 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
926 70
                    $this->computeChangeSet($targetClass, $entry);
927 70
                    break;
928
929 370
                case self::STATE_MANAGED:
930 370
                    if ($targetClass->isEmbeddedDocument) {
931 179
                        [, $knownParent ] = $this->getParentAssociation($entry);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

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

Loading history...
932 179
                        if ($knownParent && $knownParent !== $parentDocument) {
933 6
                            $entry = clone $entry;
934 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
935 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
936 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
937 3
                                $poid = spl_object_hash($parentDocument);
938 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
939 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
940
                                }
941
                            } else {
942
                                // must use unwrapped value to not trigger orphan removal
943 4
                                $unwrappedValue[$key] = $entry;
944
                            }
945 6
                            $this->persistNew($targetClass, $entry);
946
                        }
947 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
948 179
                        $this->computeChangeSet($targetClass, $entry);
949
                    }
950 370
                    break;
951
952 1
                case self::STATE_REMOVED:
953
                    // Consume the $value as array (it's either an array or an ArrayAccess)
954
                    // and remove the element from Collection.
955 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
956
                        unset($value[$key]);
957
                    }
958 1
                    break;
959
960
                case self::STATE_DETACHED:
961
                    // Can actually not happen right now as we assume STATE_NEW,
962
                    // so the exception will be raised from the DBAL layer (constraint violation).
963
                    throw new InvalidArgumentException('A detached document was found through a '
964
                        . 'relationship during cascading a persist operation.');
965
966
                default:
967
                    // MANAGED associated documents are already taken into account
968
                    // during changeset calculation anyway, since they are in the identity map.
969
            }
970
        }
971 464
    }
972
973
    /**
974
     * Computes the changeset of an individual document, independently of the
975
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
976
     *
977
     * The passed document must be a managed document. If the document already has a change set
978
     * because this method is invoked during a commit cycle then the change sets are added.
979
     * whereby changes detected in this method prevail.
980
     *
981
     * @throws InvalidArgumentException If the passed document is not MANAGED.
982
     */
983 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
984
    {
985
        // Ignore uninitialized proxy objects
986 19
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
987 1
            return;
988
        }
989
990 18
        $oid = spl_object_hash($document);
991
992 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
993
            throw new InvalidArgumentException('Document must be managed.');
994
        }
995
996 18
        if (! $class->isInheritanceTypeNone()) {
997 2
            $class = $this->dm->getClassMetadata(get_class($document));
998
        }
999
1000 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
1001 18
    }
1002
1003
    /**
1004
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1005
     */
1006 659
    private function persistNew(ClassMetadata $class, object $document) : void
1007
    {
1008 659
        $this->lifecycleEventManager->prePersist($class, $document);
1009 659
        $oid    = spl_object_hash($document);
1010 659
        $upsert = false;
1011 659
        if ($class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1358 702
            ( ! $document instanceof GhostObjectInterface || $document->isProxyInitialized())) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1359 2
            $document->addPropertyChangedListener($this);
1360
        }
1361
1362 702
        return true;
1363
    }
1364
1365
    /**
1366
     * Gets the state of a document with regard to the current unit of work.
1367
     *
1368
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1369
     *                         This parameter can be set to improve performance of document state detection
1370
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1371
     *                         is either known or does not matter for the caller of the method.
1372
     */
1373 662
    public function getDocumentState(object $document, ?int $assume = null) : int
1374
    {
1375 662
        $oid = spl_object_hash($document);
1376
1377 662
        if (isset($this->documentStates[$oid])) {
1378 413
            return $this->documentStates[$oid];
1379
        }
1380
1381 661
        $class = $this->dm->getClassMetadata(get_class($document));
1382
1383 661
        if ($class->isEmbeddedDocument) {
1384 192
            return self::STATE_NEW;
1385
        }
1386
1387 658
        if ($assume !== null) {
1388 657
            return $assume;
1389
        }
1390
1391
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1392
         * known. Note that you cannot remember the NEW or DETACHED state in
1393
         * _documentStates since the UoW does not hold references to such
1394
         * objects and the object hash can be reused. More generally, because
1395
         * the state may "change" between NEW/DETACHED without the UoW being
1396
         * aware of it.
1397
         */
1398 3
        $id = $class->getIdentifierObject($document);
1399
1400 3
        if ($id === null) {
1401 2
            return self::STATE_NEW;
1402
        }
1403
1404
        // Check for a version field, if available, to avoid a DB lookup.
1405 2
        if ($class->isVersioned && $class->versionField !== null) {
1406
            return $class->getFieldValue($document, $class->versionField)
1407
                ? self::STATE_DETACHED
1408
                : self::STATE_NEW;
1409
        }
1410
1411
        // Last try before DB lookup: check the identity map.
1412 2
        if ($this->tryGetById($id, $class)) {
1413 1
            return self::STATE_DETACHED;
1414
        }
1415
1416
        // DB lookup
1417 1
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1418 1
            return self::STATE_DETACHED;
1419
        }
1420
1421
        return self::STATE_NEW;
1422
    }
1423
1424
    /**
1425
     * Removes a document from the identity map. This effectively detaches the
1426
     * document from the persistence management of Doctrine.
1427
     *
1428
     * @internal
1429
     *
1430
     * @throws InvalidArgumentException
1431
     */
1432 97
    public function removeFromIdentityMap(object $document) : bool
1433
    {
1434 97
        $oid = spl_object_hash($document);
1435
1436
        // Check if id is registered first
1437 97
        if (! isset($this->documentIdentifiers[$oid])) {
1438
            return false;
1439
        }
1440
1441 97
        $class = $this->dm->getClassMetadata(get_class($document));
1442 97
        $id    = $this->getIdForIdentityMap($document);
1443
1444 97
        if (isset($this->identityMap[$class->name][$id])) {
1445 97
            unset($this->identityMap[$class->name][$id]);
1446 97
            $this->documentStates[$oid] = self::STATE_DETACHED;
1447
1448 97
            return true;
1449
        }
1450
1451
        return false;
1452
    }
1453
1454
    /**
1455
     * Gets a document in the identity map by its identifier hash.
1456
     *
1457
     * @internal
1458
     *
1459
     * @param mixed $id Document identifier
1460
     *
1461
     * @throws InvalidArgumentException If the class does not have an identifier.
1462
     */
1463 37
    public function getById($id, ClassMetadata $class) : object
1464
    {
1465 37
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1466
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1467
        }
1468
1469 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1470
1471 37
        return $this->identityMap[$class->name][$serializedId];
1472
    }
1473
1474
    /**
1475
     * Tries to get a document by its identifier hash. If no document is found
1476
     * for the given hash, FALSE is returned.
1477
     *
1478
     * @internal
1479
     *
1480
     * @param mixed $id Document identifier
1481
     *
1482
     * @return mixed The found document or FALSE.
1483
     *
1484
     * @throws InvalidArgumentException If the class does not have an identifier.
1485
     */
1486 313
    public function tryGetById($id, ClassMetadata $class)
1487
    {
1488 313
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1489
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1490
        }
1491
1492 313
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1493
1494 313
        return $this->identityMap[$class->name][$serializedId] ?? false;
1495
    }
1496
1497
    /**
1498
     * Schedules a document for dirty-checking at commit-time.
1499
     *
1500
     * @internal
1501
     */
1502 2
    public function scheduleForSynchronization(object $document) : void
1503
    {
1504 2
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1505 2
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1506 2
    }
1507
1508
    /**
1509
     * Checks whether a document is registered in the identity map.
1510
     *
1511
     * @internal
1512
     */
1513 90
    public function isInIdentityMap(object $document) : bool
1514
    {
1515 90
        $oid = spl_object_hash($document);
1516
1517 90
        if (! isset($this->documentIdentifiers[$oid])) {
1518 6
            return false;
1519
        }
1520
1521 88
        $class = $this->dm->getClassMetadata(get_class($document));
1522 88
        $id    = $this->getIdForIdentityMap($document);
1523
1524 88
        return isset($this->identityMap[$class->name][$id]);
1525
    }
1526
1527 702
    private function getIdForIdentityMap(object $document) : string
1528
    {
1529 702
        $class = $this->dm->getClassMetadata(get_class($document));
1530
1531 702
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1532 158
            $id = spl_object_hash($document);
1533
        } else {
1534 701
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1535 701
            $id = serialize($class->getDatabaseIdentifierValue($id));
1536
        }
1537
1538 702
        return $id;
1539
    }
1540
1541
    /**
1542
     * Checks whether an identifier exists in the identity map.
1543
     *
1544
     * @internal
1545
     */
1546
    public function containsId($id, string $rootClassName) : bool
1547
    {
1548
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1549
    }
1550
1551
    /**
1552
     * Persists a document as part of the current unit of work.
1553
     *
1554
     * @internal
1555
     *
1556
     * @throws MongoDBException If trying to persist MappedSuperclass.
1557
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1558
     */
1559 660
    public function persist(object $document) : void
1560
    {
1561 660
        $class = $this->dm->getClassMetadata(get_class($document));
1562 660
        if ($class->isMappedSuperclass || $class->isQueryResultDocument || $class->isView()) {
1563 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1564
        }
1565 659
        $visited = [];
1566 659
        $this->doPersist($document, $visited);
1567 654
    }
1568
1569
    /**
1570
     * Saves a document as part of the current unit of work.
1571
     * This method is internally called during save() cascades as it tracks
1572
     * the already visited documents to prevent infinite recursions.
1573
     *
1574
     * NOTE: This method always considers documents that are not yet known to
1575
     * this UnitOfWork as NEW.
1576
     *
1577
     * @throws InvalidArgumentException
1578
     * @throws MongoDBException
1579
     */
1580 659
    private function doPersist(object $document, array &$visited) : void
1581
    {
1582 659
        $oid = spl_object_hash($document);
1583 659
        if (isset($visited[$oid])) {
1584 25
            return; // Prevent infinite recursion
1585
        }
1586
1587 659
        $visited[$oid] = $document; // Mark visited
1588
1589 659
        $class = $this->dm->getClassMetadata(get_class($document));
1590
1591 659
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1592
        switch ($documentState) {
1593 659
            case self::STATE_MANAGED:
1594
                // Nothing to do, except if policy is "deferred explicit"
1595 57
                if ($class->isChangeTrackingDeferredExplicit()) {
1596
                    $this->scheduleForSynchronization($document);
1597
                }
1598 57
                break;
1599 659
            case self::STATE_NEW:
1600 659
                if ($class->isFile) {
1601 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1602
                }
1603
1604 658
                $this->persistNew($class, $document);
1605 656
                break;
1606
1607 2
            case self::STATE_REMOVED:
1608
                // Document becomes managed again
1609 2
                unset($this->documentDeletions[$oid]);
1610
1611 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1612 2
                break;
1613
1614
            case self::STATE_DETACHED:
1615
                throw new InvalidArgumentException(
1616
                    'Behavior of persist() for a detached document is not yet defined.'
1617
                );
1618
1619
            default:
1620
                throw MongoDBException::invalidDocumentState($documentState);
1621
        }
1622
1623 656
        $this->cascadePersist($document, $visited);
1624 654
    }
1625
1626
    /**
1627
     * Deletes a document as part of the current unit of work.
1628
     *
1629
     * @internal
1630
     */
1631 83
    public function remove(object $document)
1632
    {
1633 83
        $visited = [];
1634 83
        $this->doRemove($document, $visited);
1635 83
    }
1636
1637
    /**
1638
     * Deletes a document as part of the current unit of work.
1639
     *
1640
     * This method is internally called during delete() cascades as it tracks
1641
     * the already visited documents to prevent infinite recursions.
1642
     *
1643
     * @throws MongoDBException
1644
     */
1645 83
    private function doRemove(object $document, array &$visited) : void
1646
    {
1647 83
        $oid = spl_object_hash($document);
1648 83
        if (isset($visited[$oid])) {
1649 1
            return; // Prevent infinite recursion
1650
        }
1651
1652 83
        $visited[$oid] = $document; // mark visited
1653
1654
        /* Cascade first, because scheduleForDelete() removes the entity from
1655
         * the identity map, which can cause problems when a lazy Proxy has to
1656
         * be initialized for the cascade operation.
1657
         */
1658 83
        $this->cascadeRemove($document, $visited);
1659
1660 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1661 83
        $documentState = $this->getDocumentState($document);
1662
        switch ($documentState) {
1663 83
            case self::STATE_NEW:
1664 83
            case self::STATE_REMOVED:
1665
                // nothing to do
1666 1
                break;
1667 83
            case self::STATE_MANAGED:
1668 83
                $this->lifecycleEventManager->preRemove($class, $document);
1669 83
                $this->scheduleForDelete($document);
1670 83
                break;
1671
            case self::STATE_DETACHED:
1672
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1673
            default:
1674
                throw MongoDBException::invalidDocumentState($documentState);
1675
        }
1676 83
    }
1677
1678
    /**
1679
     * Merges the state of the given detached document into this UnitOfWork.
1680
     *
1681
     * @internal
1682
     */
1683 12
    public function merge(object $document) : object
1684
    {
1685 12
        $visited = [];
1686
1687 12
        return $this->doMerge($document, $visited);
1688
    }
1689
1690
    /**
1691
     * Executes a merge operation on a document.
1692
     *
1693
     * @throws InvalidArgumentException If the entity instance is NEW.
1694
     * @throws LockException If the document uses optimistic locking through a
1695
     *                       version attribute and the version check against the
1696
     *                       managed copy fails.
1697
     */
1698 12
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1699
    {
1700 12
        $oid = spl_object_hash($document);
1701
1702 12
        if (isset($visited[$oid])) {
1703 1
            return $visited[$oid]; // Prevent infinite recursion
1704
        }
1705
1706 12
        $visited[$oid] = $document; // mark visited
1707
1708 12
        $class = $this->dm->getClassMetadata(get_class($document));
1709
1710
        /* First we assume DETACHED, although it can still be NEW but we can
1711
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1712
         * an identity, we need to fetch it from the DB anyway in order to
1713
         * merge. MANAGED documents are ignored by the merge operation.
1714
         */
1715 12
        $managedCopy = $document;
1716
1717 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1718 12
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1719
                $document->initializeProxy();
1720
            }
1721
1722 12
            $identifier = $class->getIdentifier();
1723
            // We always have one element in the identifier array but it might be null
1724 12
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1725 12
            $managedCopy = null;
1726
1727
            // Try to fetch document from the database
1728 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1729 12
                $managedCopy = $this->dm->find($class->name, $id);
1730
1731
                // Managed copy may be removed in which case we can't merge
1732 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1733
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1734
                }
1735
1736 12
                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1737
                    $managedCopy->initializeProxy();
1738
                }
1739
            }
1740
1741 12
            if ($managedCopy === null) {
1742
                // Create a new managed instance
1743 4
                $managedCopy = $class->newInstance();
1744 4
                if ($id !== null) {
1745 3
                    $class->setIdentifierValue($managedCopy, $id);
1746
                }
1747 4
                $this->persistNew($class, $managedCopy);
1748
            }
1749
1750 12
            if ($class->isVersioned) {
1751
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1752
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1753
1754
                // Throw exception if versions don't match
1755
                if ($managedCopyVersion !== $documentVersion) {
1756
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1757
                }
1758
            }
1759
1760
            // Merge state of $document into existing (managed) document
1761 12
            foreach ($class->reflClass->getProperties() as $prop) {
1762 12
                $name = $prop->name;
1763 12
                $prop->setAccessible(true);
1764 12
                if (! isset($class->associationMappings[$name])) {
1765 12
                    if (! $class->isIdentifier($name)) {
1766 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1767
                    }
1768
                } else {
1769 12
                    $assoc2 = $class->associationMappings[$name];
1770
1771 12
                    if ($assoc2['type'] === 'one') {
1772 6
                        $other = $prop->getValue($document);
1773
1774 6
                        if ($other === null) {
1775 2
                            $prop->setValue($managedCopy, null);
1776 5
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1777
                            // Do not merge fields marked lazy that have not been fetched
1778
                            continue;
1779 5
                        } elseif (! $assoc2['isCascadeMerge']) {
1780 1
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1781 1
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1782
                                /** @var ClassMetadata $targetClass */
1783 1
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1784 1
                                $relatedId   = $targetClass->getIdentifierObject($other);
1785
1786 1
                                $current = $prop->getValue($managedCopy);
1787 1
                                if ($current !== null) {
1788 1
                                    $this->removeFromIdentityMap($current);
1789
                                }
1790
1791 1
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1792
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1793
                                } else {
1794
                                    $other = $this
1795 1
                                        ->dm
1796 1
                                        ->getProxyFactory()
1797 1
                                        ->getProxy($targetClass, $relatedId);
1798 1
                                    $this->registerManaged($other, $relatedId, []);
1799
                                }
1800
                            }
1801
1802 6
                            $prop->setValue($managedCopy, $other);
1803
                        }
1804
                    } else {
1805 11
                        $mergeCol = $prop->getValue($document);
1806
1807 11
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1808
                            /* Do not merge fields marked lazy that have not
1809
                             * been fetched. Keep the lazy persistent collection
1810
                             * of the managed copy.
1811
                             */
1812 4
                            continue;
1813
                        }
1814
1815 10
                        $managedCol = $prop->getValue($managedCopy);
1816
1817 10
                        if (! $managedCol) {
1818 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1819 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1820 1
                            $prop->setValue($managedCopy, $managedCol);
1821 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1822
                        }
1823
1824
                        /* Note: do not process association's target documents.
1825
                         * They will be handled during the cascade. Initialize
1826
                         * and, if necessary, clear $managedCol for now.
1827
                         */
1828 10
                        if ($assoc2['isCascadeMerge']) {
1829 10
                            $managedCol->initialize();
1830
1831
                            // If $managedCol differs from the merged collection, clear and set dirty
1832 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1833 3
                                $managedCol->unwrap()->clear();
1834 3
                                $managedCol->setDirty(true);
1835
1836 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1837
                                    $this->scheduleForSynchronization($managedCopy);
1838
                                }
1839
                            }
1840
                        }
1841
                    }
1842
                }
1843
1844 12
                if (! $class->isChangeTrackingNotify()) {
1845 12
                    continue;
1846
                }
1847
1848
                // Just treat all properties as changed, there is no other choice.
1849
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1850
            }
1851
1852 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1853
                $this->scheduleForSynchronization($document);
1854
            }
1855
        }
1856
1857 12
        if ($prevManagedCopy !== null) {
1858 5
            $assocField = $assoc['fieldName'];
1859 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1860
1861 5
            if ($assoc['type'] === 'one') {
1862 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1863
            } else {
1864 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1865
1866 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1867 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1868
                }
1869
            }
1870
        }
1871
1872
        // Mark the managed copy visited as well
1873 12
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1874
1875 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1876
1877 12
        return $managedCopy;
1878
    }
1879
1880
    /**
1881
     * Detaches a document from the persistence management. It's persistence will
1882
     * no longer be managed by Doctrine.
1883
     *
1884
     * @internal
1885
     */
1886 11
    public function detach(object $document) : void
1887
    {
1888 11
        $visited = [];
1889 11
        $this->doDetach($document, $visited);
1890 11
    }
1891
1892
    /**
1893
     * Executes a detach operation on the given document.
1894
     */
1895 17
    private function doDetach(object $document, array &$visited) : void
1896
    {
1897 17
        $oid = spl_object_hash($document);
1898 17
        if (isset($visited[$oid])) {
1899 3
            return; // Prevent infinite recursion
1900
        }
1901
1902 17
        $visited[$oid] = $document; // mark visited
1903
1904 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1905 17
            case self::STATE_MANAGED:
1906 17
                $this->removeFromIdentityMap($document);
1907
                unset(
1908 17
                    $this->documentInsertions[$oid],
1909 17
                    $this->documentUpdates[$oid],
1910 17
                    $this->documentDeletions[$oid],
1911 17
                    $this->documentIdentifiers[$oid],
1912 17
                    $this->documentStates[$oid],
1913 17
                    $this->originalDocumentData[$oid],
1914 17
                    $this->parentAssociations[$oid],
1915 17
                    $this->documentUpserts[$oid],
1916 17
                    $this->hasScheduledCollections[$oid],
1917 17
                    $this->embeddedDocumentsRegistry[$oid]
1918
                );
1919 17
                break;
1920 3
            case self::STATE_NEW:
1921 3
            case self::STATE_DETACHED:
1922 3
                return;
1923
        }
1924
1925 17
        $this->cascadeDetach($document, $visited);
1926 17
    }
1927
1928
    /**
1929
     * Refreshes the state of the given document from the database, overwriting
1930
     * any local, unpersisted changes.
1931
     *
1932
     * @internal
1933
     *
1934
     * @throws InvalidArgumentException If the document is not MANAGED.
1935
     */
1936 24
    public function refresh(object $document) : void
1937
    {
1938 24
        $visited = [];
1939 24
        $this->doRefresh($document, $visited);
1940 23
    }
1941
1942
    /**
1943
     * Executes a refresh operation on a document.
1944
     *
1945
     * @throws InvalidArgumentException If the document is not MANAGED.
1946
     */
1947 24
    private function doRefresh(object $document, array &$visited) : void
1948
    {
1949 24
        $oid = spl_object_hash($document);
1950 24
        if (isset($visited[$oid])) {
1951
            return; // Prevent infinite recursion
1952
        }
1953
1954 24
        $visited[$oid] = $document; // mark visited
1955
1956 24
        $class = $this->dm->getClassMetadata(get_class($document));
1957
1958 24
        if (! $class->isEmbeddedDocument) {
1959 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1960 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1961
            }
1962
1963 23
            $this->getDocumentPersister($class->name)->refresh($document);
1964
        }
1965
1966 23
        $this->cascadeRefresh($document, $visited);
1967 23
    }
1968
1969
    /**
1970
     * Cascades a refresh operation to associated documents.
1971
     */
1972 23
    private function cascadeRefresh(object $document, array &$visited) : void
1973
    {
1974 23
        $class = $this->dm->getClassMetadata(get_class($document));
1975
1976 23
        $associationMappings = array_filter(
1977 23
            $class->associationMappings,
1978
            static function ($assoc) {
1979 18
                return $assoc['isCascadeRefresh'];
1980 23
            }
1981
        );
1982
1983 23
        foreach ($associationMappings as $mapping) {
1984 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1985 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...
1986 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1987
                    // Unwrap so that foreach() does not initialize
1988 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1989
                }
1990 15
                foreach ($relatedDocuments as $relatedDocument) {
1991
                    $this->doRefresh($relatedDocument, $visited);
1992
                }
1993 10
            } elseif ($relatedDocuments !== null) {
1994 2
                $this->doRefresh($relatedDocuments, $visited);
1995
            }
1996
        }
1997 23
    }
1998
1999
    /**
2000
     * Cascades a detach operation to associated documents.
2001
     */
2002 17
    private function cascadeDetach(object $document, array &$visited) : void
2003
    {
2004 17
        $class = $this->dm->getClassMetadata(get_class($document));
2005 17
        foreach ($class->fieldMappings as $mapping) {
2006 17
            if (! $mapping['isCascadeDetach']) {
2007 17
                continue;
2008
            }
2009 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2010 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...
2011 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2012
                    // Unwrap so that foreach() does not initialize
2013 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2014
                }
2015 11
                foreach ($relatedDocuments as $relatedDocument) {
2016 8
                    $this->doDetach($relatedDocument, $visited);
2017
                }
2018 11
            } elseif ($relatedDocuments !== null) {
2019 8
                $this->doDetach($relatedDocuments, $visited);
2020
            }
2021
        }
2022 17
    }
2023
2024
    /**
2025
     * Cascades a merge operation to associated documents.
2026
     */
2027 12
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2028
    {
2029 12
        $class = $this->dm->getClassMetadata(get_class($document));
2030
2031 12
        $associationMappings = array_filter(
2032 12
            $class->associationMappings,
2033
            static function ($assoc) {
2034 12
                return $assoc['isCascadeMerge'];
2035 12
            }
2036
        );
2037
2038 12
        foreach ($associationMappings as $assoc) {
2039 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2040
2041 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...
2042 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2043
                    // Collections are the same, so there is nothing to do
2044 1
                    continue;
2045
                }
2046
2047 8
                foreach ($relatedDocuments as $relatedDocument) {
2048 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2049
                }
2050 6
            } elseif ($relatedDocuments !== null) {
2051 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2052
            }
2053
        }
2054 12
    }
2055
2056
    /**
2057
     * Cascades the save operation to associated documents.
2058
     */
2059 656
    private function cascadePersist(object $document, array &$visited) : void
2060
    {
2061 656
        $class = $this->dm->getClassMetadata(get_class($document));
2062
2063 656
        $associationMappings = array_filter(
2064 656
            $class->associationMappings,
2065
            static function ($assoc) {
2066 509
                return $assoc['isCascadePersist'];
2067 656
            }
2068
        );
2069
2070 656
        foreach ($associationMappings as $fieldName => $mapping) {
2071 457
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2072
2073 457
            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...
2074 377
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2075 15
                    if ($relatedDocuments->getOwner() !== $document) {
2076 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2077
                    }
2078
                    // Unwrap so that foreach() does not initialize
2079 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2080
                }
2081
2082 377
                $count = 0;
2083 377
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2084 201
                    if (! empty($mapping['embedded'])) {
2085 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...
2086 130
                        if ($knownParent && $knownParent !== $document) {
2087 1
                            $relatedDocument               = clone $relatedDocument;
2088 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2089
                        }
2090 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2091 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2092
                    }
2093 201
                    $this->doPersist($relatedDocument, $visited);
2094
                }
2095 357
            } elseif ($relatedDocuments !== null) {
2096 136
                if (! empty($mapping['embedded'])) {
2097 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2098 69
                    if ($knownParent && $knownParent !== $document) {
2099 3
                        $relatedDocuments = clone $relatedDocuments;
2100 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2101
                    }
2102 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2103
                }
2104 136
                $this->doPersist($relatedDocuments, $visited);
2105
            }
2106
        }
2107 654
    }
2108
2109
    /**
2110
     * Cascades the delete operation to associated documents.
2111
     */
2112 83
    private function cascadeRemove(object $document, array &$visited) : void
2113
    {
2114 83
        $class = $this->dm->getClassMetadata(get_class($document));
2115 83
        foreach ($class->fieldMappings as $mapping) {
2116 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2117 82
                continue;
2118
            }
2119 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...
2120 3
                $document->initializeProxy();
2121
            }
2122
2123 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2124 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...
2125
                // If its a PersistentCollection initialization is intended! No unwrap!
2126 31
                foreach ($relatedDocuments as $relatedDocument) {
2127 15
                    $this->doRemove($relatedDocument, $visited);
2128
                }
2129 27
            } elseif ($relatedDocuments !== null) {
2130 14
                $this->doRemove($relatedDocuments, $visited);
2131
            }
2132
        }
2133 83
    }
2134
2135
    /**
2136
     * Acquire a lock on the given document.
2137
     *
2138
     * @internal
2139
     *
2140
     * @throws LockException
2141
     * @throws InvalidArgumentException
2142
     */
2143 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2144
    {
2145 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2146 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2147
        }
2148
2149 7
        $documentName = get_class($document);
2150 7
        $class        = $this->dm->getClassMetadata($documentName);
2151
2152 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2153 2
            if (! $class->isVersioned) {
2154 1
                throw LockException::notVersioned($documentName);
2155
            }
2156
2157 1
            if ($lockVersion !== null) {
2158 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2159 1
                if ($documentVersion !== $lockVersion) {
2160 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2161
                }
2162
            }
2163 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2164 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2165
        }
2166 5
    }
2167
2168
    /**
2169
     * Releases a lock on the given document.
2170
     *
2171
     * @internal
2172
     *
2173
     * @throws InvalidArgumentException
2174
     */
2175 1
    public function unlock(object $document) : void
2176
    {
2177 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2178
            throw new InvalidArgumentException('Document is not MANAGED.');
2179
        }
2180 1
        $documentName = get_class($document);
2181 1
        $this->getDocumentPersister($documentName)->unlock($document);
2182 1
    }
2183
2184
    /**
2185
     * Clears the UnitOfWork.
2186
     *
2187
     * @internal
2188
     */
2189 383
    public function clear(?string $documentName = null) : void
2190
    {
2191 383
        if ($documentName === null) {
2192 377
            $this->identityMap                 =
2193 377
            $this->documentIdentifiers         =
2194 377
            $this->originalDocumentData        =
2195 377
            $this->documentChangeSets          =
2196 377
            $this->documentStates              =
2197 377
            $this->scheduledForSynchronization =
2198 377
            $this->documentInsertions          =
2199 377
            $this->documentUpserts             =
2200 377
            $this->documentUpdates             =
2201 377
            $this->documentDeletions           =
2202 377
            $this->collectionUpdates           =
2203 377
            $this->collectionDeletions         =
2204 377
            $this->parentAssociations          =
2205 377
            $this->embeddedDocumentsRegistry   =
2206 377
            $this->orphanRemovals              =
2207 377
            $this->hasScheduledCollections     = [];
2208
        } else {
2209 6
            $visited = [];
2210 6
            foreach ($this->identityMap as $className => $documents) {
2211 6
                if ($className !== $documentName) {
2212 3
                    continue;
2213
                }
2214
2215 6
                foreach ($documents as $document) {
2216 6
                    $this->doDetach($document, $visited);
2217
                }
2218
            }
2219
        }
2220
2221 383
        if (! $this->evm->hasListeners(Events::onClear)) {
2222 383
            return;
2223
        }
2224
2225
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2226
    }
2227
2228
    /**
2229
     * Schedules an embedded document for removal. The remove() operation will be
2230
     * invoked on that document at the beginning of the next commit of this
2231
     * UnitOfWork.
2232
     *
2233
     * @internal
2234
     */
2235 58
    public function scheduleOrphanRemoval(object $document) : void
2236
    {
2237 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2238 58
    }
2239
2240
    /**
2241
     * Unschedules an embedded or referenced object for removal.
2242
     *
2243
     * @internal
2244
     */
2245 123
    public function unscheduleOrphanRemoval(object $document) : void
2246
    {
2247 123
        $oid = spl_object_hash($document);
2248 123
        unset($this->orphanRemovals[$oid]);
2249 123
    }
2250
2251
    /**
2252
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2253
     *  1) sets owner if it was cloned
2254
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2255
     *  3) NOP if state is OK
2256
     * Returned collection should be used from now on (only important with 2nd point)
2257
     */
2258 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2259
    {
2260 8
        $owner = $coll->getOwner();
2261 8
        if ($owner === null) { // cloned
2262 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2263 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2264 2
            if (! $coll->isInitialized()) {
2265 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2266
            }
2267 2
            $newValue = clone $coll;
2268 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2269 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2270 2
            if ($this->isScheduledForUpdate($document)) {
2271
                // @todo following line should be superfluous once collections are stored in change sets
2272
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2273
            }
2274
2275 2
            return $newValue;
2276
        }
2277
2278 6
        return $coll;
2279
    }
2280
2281
    /**
2282
     * Schedules a complete collection for removal when this UnitOfWork commits.
2283
     *
2284
     * @internal
2285
     */
2286 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2287
    {
2288 47
        $oid = spl_object_hash($coll);
2289 47
        unset($this->collectionUpdates[$oid]);
2290 47
        if (isset($this->collectionDeletions[$oid])) {
2291
            return;
2292
        }
2293
2294 47
        $this->collectionDeletions[$oid] = $coll;
2295 47
        $this->scheduleCollectionOwner($coll);
2296 47
    }
2297
2298
    /**
2299
     * Checks whether a PersistentCollection is scheduled for deletion.
2300
     *
2301
     * @internal
2302
     */
2303 223
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2304
    {
2305 223
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2306
    }
2307
2308
    /**
2309
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2310
     *
2311
     * @internal
2312
     */
2313 227
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2314
    {
2315 227
        if ($coll->getOwner() === null) {
2316
            return;
2317
        }
2318
2319 227
        $oid = spl_object_hash($coll);
2320 227
        if (! isset($this->collectionDeletions[$oid])) {
2321 227
            return;
2322
        }
2323
2324 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2325 14
        unset($this->collectionDeletions[$oid]);
2326 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2327 14
    }
2328
2329
    /**
2330
     * Schedules a collection for update when this UnitOfWork commits.
2331
     *
2332
     * @internal
2333
     */
2334 253
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2335
    {
2336 253
        $mapping = $coll->getMapping();
2337 253
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2338
            /* There is no need to $unset collection if it will be $set later
2339
             * This is NOP if collection is not scheduled for deletion
2340
             */
2341 44
            $this->unscheduleCollectionDeletion($coll);
2342
        }
2343 253
        $oid = spl_object_hash($coll);
2344 253
        if (isset($this->collectionUpdates[$oid])) {
2345 11
            return;
2346
        }
2347
2348 253
        $this->collectionUpdates[$oid] = $coll;
2349 253
        $this->scheduleCollectionOwner($coll);
2350 253
    }
2351
2352
    /**
2353
     * Unschedules a collection from being updated when this UnitOfWork commits.
2354
     *
2355
     * @internal
2356
     */
2357 227
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2358
    {
2359 227
        if ($coll->getOwner() === null) {
2360
            return;
2361
        }
2362
2363 227
        $oid = spl_object_hash($coll);
2364 227
        if (! isset($this->collectionUpdates[$oid])) {
2365 52
            return;
2366
        }
2367
2368 216
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2369 216
        unset($this->collectionUpdates[$oid]);
2370 216
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2371 216
    }
2372
2373
    /**
2374
     * Checks whether a PersistentCollection is scheduled for update.
2375
     *
2376
     * @internal
2377
     */
2378 140
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2379
    {
2380 140
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2381
    }
2382
2383
    /**
2384
     * Gets PersistentCollections that have been visited during computing change
2385
     * set of $document
2386
     *
2387
     * @internal
2388
     *
2389
     * @return PersistentCollectionInterface[]
2390
     */
2391 600
    public function getVisitedCollections(object $document) : array
2392
    {
2393 600
        $oid = spl_object_hash($document);
2394
2395 600
        return $this->visitedCollections[$oid] ?? [];
2396
    }
2397
2398
    /**
2399
     * Gets PersistentCollections that are scheduled to update and related to $document
2400
     *
2401
     * @internal
2402
     *
2403
     * @return PersistentCollectionInterface[]
2404
     */
2405 600
    public function getScheduledCollections(object $document) : array
2406
    {
2407 600
        $oid = spl_object_hash($document);
2408
2409 600
        return $this->hasScheduledCollections[$oid] ?? [];
2410
    }
2411
2412
    /**
2413
     * Checks whether the document is related to a PersistentCollection
2414
     * scheduled for update or deletion.
2415
     *
2416
     * @internal
2417
     */
2418 57
    public function hasScheduledCollections(object $document) : bool
2419
    {
2420 57
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2421
    }
2422
2423
    /**
2424
     * Marks the PersistentCollection's top-level owner as having a relation to
2425
     * a collection scheduled for update or deletion.
2426
     *
2427
     * If the owner is not scheduled for any lifecycle action, it will be
2428
     * scheduled for update to ensure that versioning takes place if necessary.
2429
     *
2430
     * If the collection is nested within atomic collection, it is immediately
2431
     * unscheduled and atomic one is scheduled for update instead. This makes
2432
     * calculating update data way easier.
2433
     */
2434 255
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2435
    {
2436 255
        if ($coll->getOwner() === null) {
2437
            return;
2438
        }
2439
2440 255
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2441 255
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2442
2443 255
        if ($document !== $coll->getOwner()) {
2444 26
            $parent  = $coll->getOwner();
2445 26
            $mapping = [];
2446 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2447 26
                [$mapping, $parent ] = $parentAssoc;
2448
            }
2449 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2450 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2451 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2452 8
                $this->scheduleCollectionUpdate($atomicCollection);
2453 8
                $this->unscheduleCollectionDeletion($coll);
2454 8
                $this->unscheduleCollectionUpdate($coll);
2455
            }
2456
        }
2457
2458 255
        if ($this->isDocumentScheduled($document)) {
2459 250
            return;
2460
        }
2461
2462 55
        $this->scheduleForUpdate($document);
2463 55
    }
2464
2465
    /**
2466
     * Get the top-most owning document of a given document
2467
     *
2468
     * If a top-level document is provided, that same document will be returned.
2469
     * For an embedded document, we will walk through parent associations until
2470
     * we find a top-level document.
2471
     *
2472
     * @throws UnexpectedValueException When a top-level document could not be found.
2473
     */
2474 257
    public function getOwningDocument(object $document) : object
2475
    {
2476 257
        $class = $this->dm->getClassMetadata(get_class($document));
2477 257
        while ($class->isEmbeddedDocument) {
2478 42
            $parentAssociation = $this->getParentAssociation($document);
2479
2480 42
            if (! $parentAssociation) {
2481
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2482
            }
2483
2484 42
            [, $document ] = $parentAssociation;
2485 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2486
        }
2487
2488 257
        return $document;
2489
    }
2490
2491
    /**
2492
     * Gets the class name for an association (embed or reference) with respect
2493
     * to any discriminator value.
2494
     *
2495
     * @internal
2496
     *
2497
     * @param array|null $data
2498
     */
2499 231
    public function getClassNameForAssociation(array $mapping, $data) : string
2500
    {
2501 231
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2502
2503 231
        $discriminatorValue = null;
2504 231
        if (isset($discriminatorField, $data[$discriminatorField])) {
2505 13
            $discriminatorValue = $data[$discriminatorField];
2506 219
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2507
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2508
        }
2509
2510 231
        if ($discriminatorValue !== null) {
2511 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2512 13
                ?? (string) $discriminatorValue;
2513
        }
2514
2515 219
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2516
2517 219
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2518 11
            $discriminatorValue = $data[$class->discriminatorField];
2519 209
        } elseif ($class->defaultDiscriminatorValue !== null) {
2520 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2521
        }
2522
2523 219
        if ($discriminatorValue !== null) {
2524 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2525
        }
2526
2527 208
        return $mapping['targetDocument'];
2528
    }
2529
2530
    /**
2531
     * Creates a document. Used for reconstitution of documents during hydration.
2532
     */
2533 408
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2534
    {
2535 408
        $class = $this->dm->getClassMetadata($className);
2536
2537
        // @TODO figure out how to remove this
2538 408
        $discriminatorValue = null;
2539 408
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2540 17
            $discriminatorValue = $data[$class->discriminatorField];
2541 398
        } elseif (isset($class->defaultDiscriminatorValue)) {
2542 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2543
        }
2544
2545 408
        if ($discriminatorValue !== null) {
2546 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2547
2548 18
            $class = $this->dm->getClassMetadata($className);
2549
2550 18
            unset($data[$class->discriminatorField]);
2551
        }
2552
2553 408
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2554 2
            $document = $class->newInstance();
2555 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2556
2557 2
            return $document;
2558
        }
2559
2560 407
        $isManagedObject = false;
2561 407
        $serializedId    = null;
2562 407
        $id              = null;
2563 407
        if (! $class->isQueryResultDocument && ! $class->isView()) {
2564 403
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2565 403
            $serializedId    = serialize($id);
2566 403
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2567
        }
2568
2569 407
        $oid = null;
2570 407
        if ($isManagedObject) {
2571 105
            $document = $this->identityMap[$class->name][$serializedId];
2572 105
            $oid      = spl_object_hash($document);
2573 105
            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...
2574 16
                $document->setProxyInitializer(null);
2575 16
                $overrideLocalValues = true;
2576 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...
2577 16
                    $document->addPropertyChangedListener($this);
2578
                }
2579
            } else {
2580 95
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2581
            }
2582 105
            if ($overrideLocalValues) {
2583 44
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2584 105
                $this->originalDocumentData[$oid] = $data;
2585
            }
2586
        } else {
2587 357
            if ($document === null) {
2588 357
                $document = $class->newInstance();
2589
            }
2590
2591 357
            if (! $class->isQueryResultDocument && ! $class->isView()) {
2592 352
                $this->registerManaged($document, $id, $data);
2593 352
                $oid                                            = spl_object_hash($document);
2594 352
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2595 352
                $this->identityMap[$class->name][$serializedId] = $document;
2596
            }
2597
2598 357
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2599
2600 357
            if (! $class->isQueryResultDocument && ! $class->isView()) {
2601 352
                $this->originalDocumentData[$oid] = $data;
2602
            }
2603
        }
2604
2605 407
        return $document;
2606
    }
2607
2608
    /**
2609
     * Initializes (loads) an uninitialized persistent collection of a document.
2610
     *
2611
     * @internal
2612
     */
2613 180
    public function loadCollection(PersistentCollectionInterface $collection) : void
2614
    {
2615 180
        if ($collection->getOwner() === null) {
2616
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2617
        }
2618
2619 180
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2620 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2621 178
    }
2622
2623
    /**
2624
     * Gets the identity map of the UnitOfWork.
2625
     *
2626
     * @internal
2627
     */
2628
    public function getIdentityMap() : array
2629
    {
2630
        return $this->identityMap;
2631
    }
2632
2633
    /**
2634
     * Gets the original data of a document. The original data is the data that was
2635
     * present at the time the document was reconstituted from the database.
2636
     *
2637
     * @return array
2638
     */
2639 1
    public function getOriginalDocumentData(object $document) : array
2640
    {
2641 1
        $oid = spl_object_hash($document);
2642
2643 1
        return $this->originalDocumentData[$oid] ?? [];
2644
    }
2645
2646
    /**
2647
     * @internal
2648
     */
2649 60
    public function setOriginalDocumentData(object $document, array $data) : void
2650
    {
2651 60
        $oid                              = spl_object_hash($document);
2652 60
        $this->originalDocumentData[$oid] = $data;
2653 60
        unset($this->documentChangeSets[$oid]);
2654 60
    }
2655
2656
    /**
2657
     * Sets a property value of the original data array of a document.
2658
     *
2659
     * @internal
2660
     *
2661
     * @param mixed $value
2662
     */
2663 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2664
    {
2665 3
        $this->originalDocumentData[$oid][$property] = $value;
2666 3
    }
2667
2668
    /**
2669
     * Gets the identifier of a document.
2670
     *
2671
     * @return mixed The identifier value
2672
     */
2673 478
    public function getDocumentIdentifier(object $document)
2674
    {
2675 478
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2676
    }
2677
2678
    /**
2679
     * Checks whether the UnitOfWork has any pending insertions.
2680
     *
2681
     * @internal
2682
     *
2683
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2684
     */
2685
    public function hasPendingInsertions() : bool
2686
    {
2687
        return ! empty($this->documentInsertions);
2688
    }
2689
2690
    /**
2691
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2692
     * number of documents in the identity map.
2693
     *
2694
     * @internal
2695
     */
2696 2
    public function size() : int
2697
    {
2698 2
        $count = 0;
2699 2
        foreach ($this->identityMap as $documentSet) {
2700 2
            $count += count($documentSet);
2701
        }
2702
2703 2
        return $count;
2704
    }
2705
2706
    /**
2707
     * Registers a document as managed.
2708
     *
2709
     * TODO: This method assumes that $id is a valid PHP identifier for the
2710
     * document class. If the class expects its database identifier to be an
2711
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2712
     * document identifiers map will become inconsistent with the identity map.
2713
     * In the future, we may want to round-trip $id through a PHP and database
2714
     * conversion and throw an exception if it's inconsistent.
2715
     *
2716
     * @internal
2717
     *
2718
     * @param mixed $id The identifier values.
2719
     */
2720 390
    public function registerManaged(object $document, $id, array $data) : void
2721
    {
2722 390
        $oid   = spl_object_hash($document);
2723 390
        $class = $this->dm->getClassMetadata(get_class($document));
2724
2725 390
        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...
2726 110
            $this->documentIdentifiers[$oid] = $oid;
2727
        } else {
2728 384
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2729
        }
2730
2731 390
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2732 390
        $this->originalDocumentData[$oid] = $data;
2733 390
        $this->addToIdentityMap($document);
2734 390
    }
2735
2736
    /**
2737
     * Clears the property changeset of the document with the given OID.
2738
     *
2739
     * @internal
2740
     */
2741
    public function clearDocumentChangeSet(string $oid)
2742
    {
2743
        $this->documentChangeSets[$oid] = [];
2744
    }
2745
2746
    /* PropertyChangedListener implementation */
2747
2748
    /**
2749
     * Notifies this UnitOfWork of a property change in a document.
2750
     *
2751
     * @param object $document     The document that owns the property.
2752
     * @param string $propertyName The name of the property that changed.
2753
     * @param mixed  $oldValue     The old value of the property.
2754
     * @param mixed  $newValue     The new value of the property.
2755
     */
2756 1
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2757
    {
2758 1
        $oid   = spl_object_hash($document);
2759 1
        $class = $this->dm->getClassMetadata(get_class($document));
2760
2761 1
        if (! isset($class->fieldMappings[$propertyName])) {
2762
            return; // ignore non-persistent fields
2763
        }
2764
2765
        // Update changeset and mark document for synchronization
2766 1
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2767 1
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2768
            return;
2769
        }
2770
2771 1
        $this->scheduleForSynchronization($document);
2772 1
    }
2773
2774
    /**
2775
     * Gets the currently scheduled document insertions in this UnitOfWork.
2776
     */
2777 3
    public function getScheduledDocumentInsertions() : array
2778
    {
2779 3
        return $this->documentInsertions;
2780
    }
2781
2782
    /**
2783
     * Gets the currently scheduled document upserts in this UnitOfWork.
2784
     */
2785 1
    public function getScheduledDocumentUpserts() : array
2786
    {
2787 1
        return $this->documentUpserts;
2788
    }
2789
2790
    /**
2791
     * Gets the currently scheduled document updates in this UnitOfWork.
2792
     */
2793 2
    public function getScheduledDocumentUpdates() : array
2794
    {
2795 2
        return $this->documentUpdates;
2796
    }
2797
2798
    /**
2799
     * Gets the currently scheduled document deletions in this UnitOfWork.
2800
     */
2801
    public function getScheduledDocumentDeletions() : array
2802
    {
2803
        return $this->documentDeletions;
2804
    }
2805
2806
    /**
2807
     * Get the currently scheduled complete collection deletions
2808
     *
2809
     * @internal
2810
     */
2811
    public function getScheduledCollectionDeletions() : array
2812
    {
2813
        return $this->collectionDeletions;
2814
    }
2815
2816
    /**
2817
     * Gets the currently scheduled collection inserts, updates and deletes.
2818
     *
2819
     * @internal
2820
     */
2821
    public function getScheduledCollectionUpdates() : array
2822
    {
2823
        return $this->collectionUpdates;
2824
    }
2825
2826
    /**
2827
     * Helper method to initialize a lazy loading proxy or persistent collection.
2828
     *
2829
     * @internal
2830
     */
2831
    public function initializeObject(object $obj) : void
2832
    {
2833
        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...
2834
            $obj->initializeProxy();
2835
        } elseif ($obj instanceof PersistentCollectionInterface) {
2836
            $obj->initialize();
2837
        }
2838
    }
2839
2840
    private function objToStr(object $obj) : string
2841
    {
2842
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2843
    }
2844
}
2845