Completed
Push — master ( ecb96f...9f19a4 )
by Andreas
21s queued 13s
created

UnitOfWork::getOrCreateDocument()   D

Complexity

Conditions 15
Paths 174

Size

Total Lines 74

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 15.0032

Importance

Changes 0
Metric Value
dl 0
loc 74
ccs 40
cts 41
cp 0.9756
rs 4.8072
c 0
b 0
f 0
cc 15
nc 174
nop 4
crap 15.0032

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use function array_filter;
29
use function count;
30
use function get_class;
31
use function in_array;
32
use function is_array;
33
use function is_object;
34
use function method_exists;
35
use function preg_match;
36
use function serialize;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * The UnitOfWork is responsible for tracking changes to objects during an
42
 * "object-level" transaction and for writing out changes to the database
43
 * in the correct order.
44
 */
45
final class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    public const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    public const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    public const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    public const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = [];
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = [];
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     *
104
     * @var array
105
     */
106
    private $originalDocumentData = [];
107
108
    /**
109
     * Map of document changes. Keys are object ids (spl_object_hash).
110
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
111
     *
112
     * @var array
113
     */
114
    private $documentChangeSets = [];
115
116
    /**
117
     * The (cached) states of any known documents.
118
     * Keys are object ids (spl_object_hash).
119
     *
120
     * @var array
121
     */
122
    private $documentStates = [];
123
124
    /**
125
     * Map of documents that are scheduled for dirty checking at commit time.
126
     *
127
     * Documents are grouped by their class name, and then indexed by their SPL
128
     * object hash. This is only used for documents with a change tracking
129
     * policy of DEFERRED_EXPLICIT.
130
     *
131
     * @var array
132
     */
133
    private $scheduledForSynchronization = [];
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = [];
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = [];
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = [];
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = [];
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = [];
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = [];
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = [];
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = [];
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = [];
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = [];
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder|null
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents.
244
     *
245
     * @var array
246
     */
247
    private $parentAssociations = [];
248
249
    /** @var LifecycleEventManager */
250
    private $lifecycleEventManager;
251
252
    /**
253
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
254
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
255
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
256
     *
257
     * @var array
258
     */
259
    private $embeddedDocumentsRegistry = [];
260
261
    /** @var int */
262
    private $commitsInProgress = 0;
263
264
    /**
265
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
266
     */
267 1800
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269 1800
        $this->dm                    = $dm;
270 1800
        $this->evm                   = $evm;
271 1800
        $this->hydratorFactory       = $hydratorFactory;
272 1800
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1800
    }
274
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     *
279
     * @internal
280
     */
281 1226
    public function getPersistenceBuilder() : PersistenceBuilder
282
    {
283 1226
        if (! $this->persistenceBuilder) {
284 1226
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
285
        }
286
287 1226
        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 1216
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
320
    {
321 1216
        if (! isset($this->persisters[$documentName])) {
322 1216
            $class                           = $this->dm->getClassMetadata($documentName);
323 1216
            $pb                              = $this->getPersistenceBuilder();
324 1216
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
325
        }
326
327 1216
        return $this->persisters[$documentName];
328
    }
329
330
    /**
331
     * Get the collection persister instance.
332
     */
333 1216
    public function getCollectionPersister() : CollectionPersister
334
    {
335 1216
        if (! isset($this->collectionPersister)) {
336 1216
            $pb                        = $this->getPersistenceBuilder();
337 1216
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
338
        }
339
340 1216
        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 617
    public function commit(array $options = []) : void
367
    {
368
        // Raise preFlush
369 617
        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 617
        $this->computeChangeSets();
375
376 616
        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 616
            $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 613
        $this->commitsInProgress++;
388 613
        if ($this->commitsInProgress > 1) {
389
            throw MongoDBException::commitInProgress();
390
        }
391
        try {
392 613
            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 613
            if ($this->evm->hasListeners(Events::onFlush)) {
400 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
401
            }
402
403 612
            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 612
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
409 536
                [$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 536
                $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 599
            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 599
            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 599
            if ($this->evm->hasListeners(Events::postFlush)) {
425
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
426
            }
427
428
            // Clear up
429 599
            $this->documentInsertions          =
430 599
            $this->documentUpserts             =
431 599
            $this->documentUpdates             =
432 599
            $this->documentDeletions           =
433 599
            $this->documentChangeSets          =
434 599
            $this->collectionUpdates           =
435 599
            $this->collectionDeletions         =
436 599
            $this->visitedCollections          =
437 599
            $this->scheduledForSynchronization =
438 599
            $this->orphanRemovals              =
439 599
            $this->hasScheduledCollections     = [];
440 599
        } finally {
441 613
            $this->commitsInProgress--;
442
        }
443 599
    }
444
445
    /**
446
     * Groups a list of scheduled documents by their class.
447
     */
448 612
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
449
    {
450 612
        if (empty($documents)) {
451 612
            return [];
452
        }
453 611
454 611
        $divided = [];
455 611
        $embeds  = [];
456 611
        foreach ($documents as $oid => $d) {
457 611
            $className = get_class($d);
458 81
            if (isset($embeds[$className])) {
459
                continue;
460 611
            }
461 166
462 166
            if (isset($divided[$className])) {
463
                $divided[$className][1][$oid] = $d;
464 611
                continue;
465 611
            }
466 183
467 183
            $class = $this->dm->getClassMetadata($className);
468
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
469 611
                $embeds[$className] = true;
470 611
                continue;
471
            }
472 4
473
            if ($class->isView()) {
474
                continue;
475
            }
476 611
477
            if (empty($divided[$class->name])) {
478
                $divided[$class->name] = [$class, [$oid => $d]];
479
            } else {
480
                $divided[$class->name][1][$oid] = $d;
481
            }
482
        }
483
484 622
        return $divided;
485
    }
486 622
487 549
    /**
488 549
     * Compute changesets of all documents scheduled for insertion.
489 165
     *
490
     * Embedded documents will not be processed.
491
     */
492 543
    private function computeScheduleInsertsChangeSets() : void
493
    {
494 621
        foreach ($this->documentInsertions as $document) {
495
            $class = $this->dm->getClassMetadata(get_class($document));
496
            if ($class->isEmbeddedDocument || $class->isView()) {
497
                continue;
498
            }
499
500
            $this->computeChangeSet($class, $document);
501 621
        }
502
    }
503 621
504 85
    /**
505 85
     * Compute changesets of all documents scheduled for upsert.
506
     *
507
     * Embedded documents will not be processed.
508
     */
509 85
    private function computeScheduleUpsertsChangeSets() : void
510
    {
511 621
        foreach ($this->documentUpserts as $document) {
512
            $class = $this->dm->getClassMetadata(get_class($document));
513
            if ($class->isEmbeddedDocument || $class->isView()) {
514
                continue;
515
            }
516
517
            $this->computeChangeSet($class, $document);
518 627
        }
519
    }
520 627
521
    /**
522 627
     * Gets the changeset for a document.
523
     *
524
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
525
     */
526
    public function getDocumentChangeSet(object $document) : array
527
    {
528
        $oid = spl_object_hash($document);
529
530 1
        return $this->documentChangeSets[$oid] ?? [];
531
    }
532 1
533 1
    /**
534
     * Sets the changeset for a document.
535
     *
536
     * @internal
537
     */
538
    public function setDocumentChangeSet(object $document, array $changeset) : void
539
    {
540
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
541
    }
542 630
543
    /**
544 630
     * Get a documents actual data, flattening all the objects to arrays.
545 630
     *
546 630
     * @internal
547 630
     *
548
     * @return array
549 630
     */
550 202
    public function getDocumentActualData(object $document) : array
551
    {
552 630
        $class      = $this->dm->getClassMetadata(get_class($document));
553 630
        $actualData = [];
554 630
        foreach ($class->reflFields as $name => $refProp) {
555
            $mapping = $class->fieldMappings[$name];
556 405
            // skip not saved fields
557 147
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
558
                continue;
559
            }
560
            $value = $refProp->getValue($document);
561 405
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
562 405
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
563 405
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
564 405
                if (! $value instanceof Collection) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
855 294
                    continue;
856
                }
857
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
858 621
                $oid = spl_object_hash($document);
859
                if (isset($this->documentInsertions[$oid])
860
                    || isset($this->documentUpserts[$oid])
861
                    || isset($this->documentDeletions[$oid])
862
                    || ! isset($this->documentStates[$oid])
863
                ) {
864
                    continue;
865
                }
866
867 465
                $this->computeChangeSet($class, $document);
868
            }
869 465
        }
870 465
    }
871 465
872
    /**
873 465
     * Computes the changes of an association.
874 7
     *
875
     * @param mixed $value The value of the association.
876
     *
877 464
     * @throws InvalidArgumentException
878 257
     */
879 253
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
880
    {
881
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
882 257
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
883 257
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
884 257
885 151
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
999
            return;
1000 18
        }
1001 18
1002
        $oid = spl_object_hash($document);
1003
1004
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
1005
            throw new InvalidArgumentException('Document must be managed.');
1006 658
        }
1007
1008 658
        if (! $class->isInheritanceTypeNone()) {
1009 658
            $class = $this->dm->getClassMetadata(get_class($document));
1010 658
        }
1011 658
1012 658
        $this->computeOrRecomputeChangeSet($class, $document, true);
1013 658
    }
1014
1015 658
    /**
1016 3
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1017 3
     */
1018 3
    private function persistNew(ClassMetadata $class, object $document) : void
1019
    {
1020
        $this->lifecycleEventManager->prePersist($class, $document);
1021
        $oid    = spl_object_hash($document);
1022 657
        $upsert = false;
1023 1
        if ($class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1482
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1483
        }
1484
1485
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1486 313
1487
        return $this->identityMap[$class->name][$serializedId];
1488 313
    }
1489
1490
    /**
1491
     * Tries to get a document by its identifier hash. If no document is found
1492 313
     * for the given hash, FALSE is returned.
1493
     *
1494 313
     * @internal
1495
     *
1496
     * @param mixed $id Document identifier
1497
     *
1498
     * @return mixed The found document or FALSE.
1499
     *
1500
     * @throws InvalidArgumentException If the class does not have an identifier.
1501
     */
1502 2
    public function tryGetById($id, ClassMetadata $class)
1503
    {
1504 2
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1505 2
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1506 2
        }
1507
1508
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1509
1510
        return $this->identityMap[$class->name][$serializedId] ?? false;
1511
    }
1512
1513 90
    /**
1514
     * Schedules a document for dirty-checking at commit-time.
1515 90
     *
1516
     * @internal
1517 90
     */
1518 6
    public function scheduleForSynchronization(object $document) : void
1519
    {
1520
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1521 88
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1522 88
    }
1523
1524 88
    /**
1525
     * Checks whether a document is registered in the identity map.
1526
     *
1527 701
     * @internal
1528
     */
1529 701
    public function isInIdentityMap(object $document) : bool
1530
    {
1531 701
        $oid = spl_object_hash($document);
1532 158
1533
        if (! isset($this->documentIdentifiers[$oid])) {
1534 700
            return false;
1535 700
        }
1536
1537
        $class = $this->dm->getClassMetadata(get_class($document));
1538 701
        $id    = $this->getIdForIdentityMap($document);
1539
1540
        return isset($this->identityMap[$class->name][$id]);
1541
    }
1542
1543
    private function getIdForIdentityMap(object $document) : string
1544
    {
1545
        $class = $this->dm->getClassMetadata(get_class($document));
1546
1547
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1739
                $document->initializeProxy();
1740
            }
1741 12
1742
            $identifier = $class->getIdentifier();
1743 4
            // We always have one element in the identifier array but it might be null
1744 4
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1745 3
            $managedCopy = null;
1746
1747 4
            // Try to fetch document from the database
1748
            if (! $class->isEmbeddedDocument && $id !== null) {
1749
                $managedCopy = $this->dm->find($class->name, $id);
1750 12
1751
                // Managed copy may be removed in which case we can't merge
1752
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1753
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1754
                }
1755
1756
                if ($managedCopy instanceof GhostObjectInterface && ! $managedCopy->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1757
                    $managedCopy->initializeProxy();
1758
                }
1759
            }
1760
1761 12
            if ($managedCopy === null) {
1762 12
                // Create a new managed instance
1763 12
                $managedCopy = $class->newInstance();
1764 12
                if ($id !== null) {
1765 12
                    $class->setIdentifierValue($managedCopy, $id);
1766 12
                }
1767
                $this->persistNew($class, $managedCopy);
1768
            }
1769 12
1770
            if ($class->isVersioned) {
1771 12
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1772 6
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1773
1774 6
                // Throw exception if versions don't match
1775 2
                if ($managedCopyVersion !== $documentVersion) {
1776 5
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1777
                }
1778
            }
1779 5
1780 1
            // Merge state of $document into existing (managed) document
1781 1
            foreach ($class->reflClass->getProperties() as $prop) {
1782
                $name = $prop->name;
1783 1
                $prop->setAccessible(true);
1784 1
                if (! isset($class->associationMappings[$name])) {
1785
                    if (! $class->isIdentifier($name)) {
1786 1
                        $prop->setValue($managedCopy, $prop->getValue($document));
1787 1
                    }
1788 1
                } else {
1789
                    $assoc2 = $class->associationMappings[$name];
1790
1791 1
                    if ($assoc2['type'] === 'one') {
1792
                        $other = $prop->getValue($document);
1793
1794
                        if ($other === null) {
1795 1
                            $prop->setValue($managedCopy, null);
1796 1
                        } elseif ($other instanceof GhostObjectInterface && ! $other->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1797 1
                            // Do not merge fields marked lazy that have not been fetched
1798 1
                            continue;
1799
                        } elseif (! $assoc2['isCascadeMerge']) {
1800
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1801
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1802 6
                                /** @var ClassMetadata $targetClass */
1803
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1804
                                $relatedId   = $targetClass->getIdentifierObject($other);
1805 11
1806
                                $current = $prop->getValue($managedCopy);
1807 11
                                if ($current !== null) {
1808
                                    $this->removeFromIdentityMap($current);
1809
                                }
1810
1811
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
1812 4
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1813
                                } else {
1814
                                    $other = $this
1815 10
                                        ->dm
1816
                                        ->getProxyFactory()
1817 10
                                        ->getProxy($targetClass, $relatedId);
1818 1
                                    $this->registerManaged($other, $relatedId, []);
1819 1
                                }
1820 1
                            }
1821 1
1822
                            $prop->setValue($managedCopy, $other);
1823
                        }
1824
                    } else {
1825
                        $mergeCol = $prop->getValue($document);
1826
1827
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1828 10
                            /* Do not merge fields marked lazy that have not
1829 10
                             * been fetched. Keep the lazy persistent collection
1830
                             * of the managed copy.
1831
                             */
1832 10
                            continue;
1833 3
                        }
1834 3
1835
                        $managedCol = $prop->getValue($managedCopy);
1836 3
1837
                        if (! $managedCol) {
1838
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1839
                            $managedCol->setOwner($managedCopy, $assoc2);
1840
                            $prop->setValue($managedCopy, $managedCol);
1841
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1842
                        }
1843
1844 12
                        /* Note: do not process association's target documents.
1845 12
                         * They will be handled during the cascade. Initialize
1846
                         * and, if necessary, clear $managedCol for now.
1847
                         */
1848
                        if ($assoc2['isCascadeMerge']) {
1849
                            $managedCol->initialize();
1850
1851
                            // If $managedCol differs from the merged collection, clear and set dirty
1852 12
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1853
                                $managedCol->unwrap()->clear();
1854
                                $managedCol->setDirty(true);
1855
1856
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1857 12
                                    $this->scheduleForSynchronization($managedCopy);
1858 5
                                }
1859 5
                            }
1860
                        }
1861 5
                    }
1862 3
                }
1863
1864 4
                if (! $class->isChangeTrackingNotify()) {
1865
                    continue;
1866 4
                }
1867 1
1868
                // Just treat all properties as changed, there is no other choice.
1869
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1870
            }
1871
1872
            if ($class->isChangeTrackingDeferredExplicit()) {
1873 12
                $this->scheduleForSynchronization($document);
1874
            }
1875 12
        }
1876
1877 12
        if ($prevManagedCopy !== null) {
1878
            $assocField = $assoc['fieldName'];
1879
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1880
1881
            if ($assoc['type'] === 'one') {
1882
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1883
            } else {
1884
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1885
1886 11
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1887
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1888 11
                }
1889 11
            }
1890 11
        }
1891
1892
        // Mark the managed copy visited as well
1893
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1894
1895 17
        $this->cascadeMerge($document, $managedCopy, $visited);
1896
1897 17
        return $managedCopy;
1898 17
    }
1899 3
1900
    /**
1901
     * Detaches a document from the persistence management. It's persistence will
1902 17
     * no longer be managed by Doctrine.
1903
     *
1904 17
     * @internal
1905 17
     */
1906 17
    public function detach(object $document) : void
1907
    {
1908 17
        $visited = [];
1909 17
        $this->doDetach($document, $visited);
1910 17
    }
1911 17
1912 17
    /**
1913 17
     * Executes a detach operation on the given document.
1914 17
     */
1915 17
    private function doDetach(object $document, array &$visited) : void
1916 17
    {
1917 17
        $oid = spl_object_hash($document);
1918
        if (isset($visited[$oid])) {
1919 17
            return; // Prevent infinite recursion
1920 3
        }
1921 3
1922 3
        $visited[$oid] = $document; // mark visited
1923
1924
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1925 17
            case self::STATE_MANAGED:
1926 17
                $this->removeFromIdentityMap($document);
1927
                unset(
1928
                    $this->documentInsertions[$oid],
1929
                    $this->documentUpdates[$oid],
1930
                    $this->documentDeletions[$oid],
1931
                    $this->documentIdentifiers[$oid],
1932
                    $this->documentStates[$oid],
1933
                    $this->originalDocumentData[$oid],
1934
                    $this->parentAssociations[$oid],
1935
                    $this->documentUpserts[$oid],
1936 24
                    $this->hasScheduledCollections[$oid],
1937
                    $this->embeddedDocumentsRegistry[$oid]
1938 24
                );
1939 24
                break;
1940 23
            case self::STATE_NEW:
1941
            case self::STATE_DETACHED:
1942
                return;
1943
        }
1944
1945
        $this->cascadeDetach($document, $visited);
1946
    }
1947 24
1948
    /**
1949 24
     * Refreshes the state of the given document from the database, overwriting
1950 24
     * any local, unpersisted changes.
1951
     *
1952
     * @internal
1953
     *
1954 24
     * @throws InvalidArgumentException If the document is not MANAGED.
1955
     */
1956 24
    public function refresh(object $document) : void
1957
    {
1958 24
        $visited = [];
1959 24
        $this->doRefresh($document, $visited);
1960 1
    }
1961
1962
    /**
1963 23
     * Executes a refresh operation on a document.
1964
     *
1965
     * @throws InvalidArgumentException If the document is not MANAGED.
1966 23
     */
1967 23
    private function doRefresh(object $document, array &$visited) : void
1968
    {
1969
        $oid = spl_object_hash($document);
1970
        if (isset($visited[$oid])) {
1971
            return; // Prevent infinite recursion
1972 23
        }
1973
1974 23
        $visited[$oid] = $document; // mark visited
1975
1976 23
        $class = $this->dm->getClassMetadata(get_class($document));
1977 23
1978
        if (! $class->isEmbeddedDocument) {
1979 18
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1980 23
                throw new InvalidArgumentException('Document is not MANAGED.');
1981
            }
1982
1983 23
            $this->getDocumentPersister($class->name)->refresh($document);
1984 15
        }
1985 15
1986 15
        $this->cascadeRefresh($document, $visited);
1987
    }
1988 15
1989
    /**
1990 15
     * Cascades a refresh operation to associated documents.
1991
     */
1992
    private function cascadeRefresh(object $document, array &$visited) : void
1993 10
    {
1994 2
        $class = $this->dm->getClassMetadata(get_class($document));
1995
1996
        $associationMappings = array_filter(
1997 23
            $class->associationMappings,
1998
            static function ($assoc) {
1999
                return $assoc['isCascadeRefresh'];
2000
            }
2001
        );
2002 17
2003
        foreach ($associationMappings as $mapping) {
2004 17
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2005 17
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2006 17
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2007 17
                    // Unwrap so that foreach() does not initialize
2008
                    $relatedDocuments = $relatedDocuments->unwrap();
2009 11
                }
2010 11
                foreach ($relatedDocuments as $relatedDocument) {
2011 11
                    $this->doRefresh($relatedDocument, $visited);
2012
                }
2013 8
            } elseif ($relatedDocuments !== null) {
2014
                $this->doRefresh($relatedDocuments, $visited);
2015 11
            }
2016 8
        }
2017
    }
2018 11
2019 8
    /**
2020
     * Cascades a detach operation to associated documents.
2021
     */
2022 17
    private function cascadeDetach(object $document, array &$visited) : void
2023
    {
2024
        $class = $this->dm->getClassMetadata(get_class($document));
2025
        foreach ($class->fieldMappings as $mapping) {
2026
            if (! $mapping['isCascadeDetach']) {
2027 12
                continue;
2028
            }
2029 12
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2030
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2031 12
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2032 12
                    // Unwrap so that foreach() does not initialize
2033
                    $relatedDocuments = $relatedDocuments->unwrap();
2034 12
                }
2035 12
                foreach ($relatedDocuments as $relatedDocument) {
2036
                    $this->doDetach($relatedDocument, $visited);
2037
                }
2038 12
            } elseif ($relatedDocuments !== null) {
2039 11
                $this->doDetach($relatedDocuments, $visited);
2040
            }
2041 11
        }
2042 8
    }
2043
2044 1
    /**
2045
     * Cascades a merge operation to associated documents.
2046
     */
2047 8
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2048 4
    {
2049
        $class = $this->dm->getClassMetadata(get_class($document));
2050 6
2051 4
        $associationMappings = array_filter(
2052
            $class->associationMappings,
2053
            static function ($assoc) {
2054 12
                return $assoc['isCascadeMerge'];
2055
            }
2056
        );
2057
2058
        foreach ($associationMappings as $assoc) {
2059 655
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2060
2061 655
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2062
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2063 655
                    // Collections are the same, so there is nothing to do
2064 655
                    continue;
2065
                }
2066 508
2067 655
                foreach ($relatedDocuments as $relatedDocument) {
2068
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2069
                }
2070 655
            } elseif ($relatedDocuments !== null) {
2071 456
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2072
            }
2073 456
        }
2074 376
    }
2075 15
2076 2
    /**
2077
     * Cascades the save operation to associated documents.
2078
     */
2079 15
    private function cascadePersist(object $document, array &$visited) : void
2080
    {
2081
        $class = $this->dm->getClassMetadata(get_class($document));
2082 376
2083 376
        $associationMappings = array_filter(
2084 201
            $class->associationMappings,
2085 130
            static function ($assoc) {
2086 130
                return $assoc['isCascadePersist'];
2087 1
            }
2088 1
        );
2089
2090 130
        foreach ($associationMappings as $fieldName => $mapping) {
2091 130
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2092
2093 201
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2094
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2095 356
                    if ($relatedDocuments->getOwner() !== $document) {
2096 136
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2097 69
                    }
2098 69
                    // Unwrap so that foreach() does not initialize
2099 3
                    $relatedDocuments = $relatedDocuments->unwrap();
2100 3
                }
2101
2102 69
                $count = 0;
2103
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2104 136
                    if (! empty($mapping['embedded'])) {
2105
                        [, $knownParent ] = $this->getParentAssociation($relatedDocument);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

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

Loading history...
2106
                        if ($knownParent && $knownParent !== $document) {
2107 653
                            $relatedDocument               = clone $relatedDocument;
2108
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2109
                        }
2110
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2111
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2112 83
                    }
2113
                    $this->doPersist($relatedDocument, $visited);
2114 83
                }
2115 83
            } elseif ($relatedDocuments !== null) {
2116 83
                if (! empty($mapping['embedded'])) {
2117 82
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2118
                    if ($knownParent && $knownParent !== $document) {
2119 43
                        $relatedDocuments = clone $relatedDocuments;
2120 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2121
                    }
2122
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2123 43
                }
2124 43
                $this->doPersist($relatedDocuments, $visited);
2125
            }
2126 31
        }
2127 15
    }
2128
2129 27
    /**
2130 14
     * Cascades the delete operation to associated documents.
2131
     */
2132
    private function cascadeRemove(object $document, array &$visited) : void
2133 83
    {
2134
        $class = $this->dm->getClassMetadata(get_class($document));
2135
        foreach ($class->fieldMappings as $mapping) {
2136
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2137
                continue;
2138
            }
2139
            if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2594 352
                $document->setProxyInitializer(null);
2595 352
                $overrideLocalValues = true;
2596
                if ($document instanceof NotifyPropertyChanged) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\NotifyPropertyChanged does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2597
                    $document->addPropertyChangedListener($this);
2598 356
                }
2599
            } else {
2600 356
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2601 352
            }
2602
            if ($overrideLocalValues) {
2603
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2604
                $this->originalDocumentData[$oid] = $data;
2605 406
            }
2606
        } else {
2607
            if ($document === null) {
2608
                $document = $class->newInstance();
2609
            }
2610
2611
            if (! $class->isQueryResultDocument) {
2612
                $this->registerManaged($document, $id, $data);
2613 180
                $oid                                            = spl_object_hash($document);
2614
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2615 180
                $this->identityMap[$class->name][$serializedId] = $document;
2616
            }
2617
2618
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2619 180
2620 178
            if (! $class->isQueryResultDocument && ! $class->isView()) {
2621 178
                $this->originalDocumentData[$oid] = $data;
2622
            }
2623
        }
2624
2625
        return $document;
2626
    }
2627
2628
    /**
2629
     * Initializes (loads) an uninitialized persistent collection of a document.
2630
     *
2631
     * @internal
2632
     */
2633
    public function loadCollection(PersistentCollectionInterface $collection) : void
2634
    {
2635
        if ($collection->getOwner() === null) {
2636
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2637
        }
2638
2639 1
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2640
        $this->lifecycleEventManager->postCollectionLoad($collection);
2641 1
    }
2642
2643 1
    /**
2644
     * Gets the identity map of the UnitOfWork.
2645
     *
2646
     * @internal
2647
     */
2648
    public function getIdentityMap() : array
2649 60
    {
2650
        return $this->identityMap;
2651 60
    }
2652 60
2653 60
    /**
2654 60
     * Gets the original data of a document. The original data is the data that was
2655
     * present at the time the document was reconstituted from the database.
2656
     *
2657
     * @return array
2658
     */
2659
    public function getOriginalDocumentData(object $document) : array
2660
    {
2661
        $oid = spl_object_hash($document);
2662
2663 3
        return $this->originalDocumentData[$oid] ?? [];
2664
    }
2665 3
2666 3
    /**
2667
     * @internal
2668
     */
2669
    public function setOriginalDocumentData(object $document, array $data) : void
2670
    {
2671
        $oid                              = spl_object_hash($document);
2672
        $this->originalDocumentData[$oid] = $data;
2673 478
        unset($this->documentChangeSets[$oid]);
2674
    }
2675 478
2676
    /**
2677
     * Sets a property value of the original data array of a document.
2678
     *
2679
     * @internal
2680
     *
2681
     * @param mixed $value
2682
     */
2683
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2684
    {
2685
        $this->originalDocumentData[$oid][$property] = $value;
2686
    }
2687
2688
    /**
2689
     * Gets the identifier of a document.
2690
     *
2691
     * @return mixed The identifier value
2692
     */
2693
    public function getDocumentIdentifier(object $document)
2694
    {
2695
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2696 2
    }
2697
2698 2
    /**
2699 2
     * Checks whether the UnitOfWork has any pending insertions.
2700 2
     *
2701
     * @internal
2702
     *
2703 2
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2704
     */
2705
    public function hasPendingInsertions() : bool
2706
    {
2707
        return ! empty($this->documentInsertions);
2708
    }
2709
2710
    /**
2711
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2712
     * number of documents in the identity map.
2713
     *
2714
     * @internal
2715
     */
2716
    public function size() : int
2717
    {
2718
        $count = 0;
2719
        foreach ($this->identityMap as $documentSet) {
2720 390
            $count += count($documentSet);
2721
        }
2722 390
2723 390
        return $count;
2724
    }
2725 390
2726 110
    /**
2727
     * Registers a document as managed.
2728 384
     *
2729
     * TODO: This method assumes that $id is a valid PHP identifier for the
2730
     * document class. If the class expects its database identifier to be an
2731 390
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2732 390
     * document identifiers map will become inconsistent with the identity map.
2733 390
     * In the future, we may want to round-trip $id through a PHP and database
2734 390
     * conversion and throw an exception if it's inconsistent.
2735
     *
2736
     * @internal
2737
     *
2738
     * @param mixed $id The identifier values.
2739
     */
2740
    public function registerManaged(object $document, $id, array $data) : void
2741
    {
2742
        $oid   = spl_object_hash($document);
2743
        $class = $this->dm->getClassMetadata(get_class($document));
2744
2745
        if (! $class->identifier || $id === null) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2746
            $this->documentIdentifiers[$oid] = $oid;
2747
        } else {
2748
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2749
        }
2750
2751
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2752
        $this->originalDocumentData[$oid] = $data;
2753
        $this->addToIdentityMap($document);
2754
    }
2755
2756 1
    /**
2757
     * Clears the property changeset of the document with the given OID.
2758 1
     *
2759 1
     * @internal
2760
     */
2761 1
    public function clearDocumentChangeSet(string $oid)
2762
    {
2763
        $this->documentChangeSets[$oid] = [];
2764
    }
2765
2766 1
    /* PropertyChangedListener implementation */
2767 1
2768
    /**
2769
     * Notifies this UnitOfWork of a property change in a document.
2770
     *
2771 1
     * @param object $document     The document that owns the property.
2772 1
     * @param string $propertyName The name of the property that changed.
2773
     * @param mixed  $oldValue     The old value of the property.
2774
     * @param mixed  $newValue     The new value of the property.
2775
     */
2776
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2777 3
    {
2778
        $oid   = spl_object_hash($document);
2779 3
        $class = $this->dm->getClassMetadata(get_class($document));
2780
2781
        if (! isset($class->fieldMappings[$propertyName])) {
2782
            return; // ignore non-persistent fields
2783
        }
2784
2785 1
        // Update changeset and mark document for synchronization
2786
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2787 1
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2788
            return;
2789
        }
2790
2791
        $this->scheduleForSynchronization($document);
2792
    }
2793 2
2794
    /**
2795 2
     * Gets the currently scheduled document insertions in this UnitOfWork.
2796
     */
2797
    public function getScheduledDocumentInsertions() : array
2798
    {
2799
        return $this->documentInsertions;
2800
    }
2801
2802
    /**
2803
     * Gets the currently scheduled document upserts in this UnitOfWork.
2804
     */
2805
    public function getScheduledDocumentUpserts() : array
2806
    {
2807
        return $this->documentUpserts;
2808
    }
2809
2810
    /**
2811
     * Gets the currently scheduled document updates in this UnitOfWork.
2812
     */
2813
    public function getScheduledDocumentUpdates() : array
2814
    {
2815
        return $this->documentUpdates;
2816
    }
2817
2818
    /**
2819
     * Gets the currently scheduled document deletions in this UnitOfWork.
2820
     */
2821
    public function getScheduledDocumentDeletions() : array
2822
    {
2823
        return $this->documentDeletions;
2824
    }
2825
2826
    /**
2827
     * Get the currently scheduled complete collection deletions
2828
     *
2829
     * @internal
2830
     */
2831
    public function getScheduledCollectionDeletions() : array
2832
    {
2833
        return $this->collectionDeletions;
2834
    }
2835
2836
    /**
2837
     * Gets the currently scheduled collection inserts, updates and deletes.
2838
     *
2839
     * @internal
2840
     */
2841
    public function getScheduledCollectionUpdates() : array
2842
    {
2843
        return $this->collectionUpdates;
2844
    }
2845
2846
    /**
2847
     * Helper method to initialize a lazy loading proxy or persistent collection.
2848
     *
2849
     * @internal
2850
     */
2851
    public function initializeObject(object $obj) : void
2852
    {
2853
        if ($obj instanceof GhostObjectInterface) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2854
            $obj->initializeProxy();
2855
        } elseif ($obj instanceof PersistentCollectionInterface) {
2856
            $obj->initialize();
2857
        }
2858
    }
2859
2860
    private function objToStr(object $obj) : string
2861
    {
2862
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2863
    }
2864
}
2865