Completed
Pull Request — master (#1803)
by Andreas
16:13
created

UnitOfWork::scheduleCollectionOwner()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 19
cts 20
cp 0.95
rs 8.8177
c 0
b 0
f 0
cc 6
nc 7
nop 1
crap 6.0045
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use function array_filter;
29
use function count;
30
use function get_class;
31
use function in_array;
32
use function is_array;
33
use function is_object;
34
use function method_exists;
35
use function preg_match;
36
use function serialize;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * The UnitOfWork is responsible for tracking changes to objects during an
42
 * "object-level" transaction and for writing out changes to the database
43
 * in the correct order.
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    public const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    public const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    public const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    public const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = [];
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = [];
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     *
104
     * @var array
105
     */
106
    private $originalDocumentData = [];
107
108
    /**
109
     * Map of document changes. Keys are object ids (spl_object_hash).
110
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
111
     *
112
     * @var array
113
     */
114
    private $documentChangeSets = [];
115
116
    /**
117
     * The (cached) states of any known documents.
118
     * Keys are object ids (spl_object_hash).
119
     *
120
     * @var array
121
     */
122
    private $documentStates = [];
123
124
    /**
125
     * Map of documents that are scheduled for dirty checking at commit time.
126
     *
127
     * Documents are grouped by their class name, and then indexed by their SPL
128
     * object hash. This is only used for documents with a change tracking
129
     * policy of DEFERRED_EXPLICIT.
130
     *
131
     * @var array
132
     * @todo rename: scheduledForSynchronization
133
     */
134
    private $scheduledForDirtyCheck = [];
135
136
    /**
137
     * A list of all pending document insertions.
138
     *
139
     * @var array
140
     */
141
    private $documentInsertions = [];
142
143
    /**
144
     * A list of all pending document updates.
145
     *
146
     * @var array
147
     */
148
    private $documentUpdates = [];
149
150
    /**
151
     * A list of all pending document upserts.
152
     *
153
     * @var array
154
     */
155
    private $documentUpserts = [];
156
157
    /**
158
     * A list of all pending document deletions.
159
     *
160
     * @var array
161
     */
162
    private $documentDeletions = [];
163
164
    /**
165
     * All pending collection deletions.
166
     *
167
     * @var array
168
     */
169
    private $collectionDeletions = [];
170
171
    /**
172
     * All pending collection updates.
173
     *
174
     * @var array
175
     */
176
    private $collectionUpdates = [];
177
178
    /**
179
     * A list of documents related to collections scheduled for update or deletion
180
     *
181
     * @var array
182
     */
183
    private $hasScheduledCollections = [];
184
185
    /**
186
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
187
     * At the end of the UnitOfWork all these collections will make new snapshots
188
     * of their data.
189
     *
190
     * @var array
191
     */
192
    private $visitedCollections = [];
193
194
    /**
195
     * The DocumentManager that "owns" this UnitOfWork instance.
196
     *
197
     * @var DocumentManager
198
     */
199
    private $dm;
200
201
    /**
202
     * The EventManager used for dispatching events.
203
     *
204
     * @var EventManager
205
     */
206
    private $evm;
207
208
    /**
209
     * Additional documents that are scheduled for removal.
210
     *
211
     * @var array
212
     */
213
    private $orphanRemovals = [];
214
215
    /**
216
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
217
     *
218
     * @var HydratorFactory
219
     */
220
    private $hydratorFactory;
221
222
    /**
223
     * The document persister instances used to persist document instances.
224
     *
225
     * @var array
226
     */
227
    private $persisters = [];
228
229
    /**
230
     * The collection persister instance used to persist changes to collections.
231
     *
232
     * @var Persisters\CollectionPersister
233
     */
234
    private $collectionPersister;
235
236
    /**
237
     * The persistence builder instance used in DocumentPersisters.
238
     *
239
     * @var PersistenceBuilder|null
240
     */
241
    private $persistenceBuilder;
242
243
    /**
244
     * Array of parent associations between embedded documents.
245
     *
246
     * @var array
247
     */
248
    private $parentAssociations = [];
249
250
    /** @var LifecycleEventManager */
251
    private $lifecycleEventManager;
252
253
    /**
254
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
255
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
256
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
257
     *
258
     * @var array
259
     */
260
    private $embeddedDocumentsRegistry = [];
261
262
    /** @var int */
263
    private $commitsInProgress = 0;
264
265
    /**
266
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
267
     */
268 1642
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
269
    {
270 1642
        $this->dm                    = $dm;
271 1642
        $this->evm                   = $evm;
272 1642
        $this->hydratorFactory       = $hydratorFactory;
273 1642
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
274 1642
    }
275
276
    /**
277
     * Factory for returning new PersistenceBuilder instances used for preparing data into
278
     * queries for insert persistence.
279
     */
280 1123
    public function getPersistenceBuilder() : PersistenceBuilder
281
    {
282 1123
        if (! $this->persistenceBuilder) {
283 1123
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
284
        }
285 1123
        return $this->persistenceBuilder;
286
    }
287
288
    /**
289
     * Sets the parent association for a given embedded document.
290
     */
291 202
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
292
    {
293 202
        $oid                                   = spl_object_hash($document);
294 202
        $this->embeddedDocumentsRegistry[$oid] = $document;
295 202
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
296 202
    }
297
298
    /**
299
     * Gets the parent association for a given embedded document.
300
     *
301
     *     <code>
302
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
303
     *     </code>
304
     */
305 226
    public function getParentAssociation(object $document) : ?array
306
    {
307 226
        $oid = spl_object_hash($document);
308
309 226
        return $this->parentAssociations[$oid] ?? null;
310
    }
311
312
    /**
313
     * Get the document persister instance for the given document name
314
     */
315 1121
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
316
    {
317 1121
        if (! isset($this->persisters[$documentName])) {
318 1108
            $class                           = $this->dm->getClassMetadata($documentName);
319 1108
            $pb                              = $this->getPersistenceBuilder();
320 1108
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
321
        }
322 1121
        return $this->persisters[$documentName];
323
    }
324
325
    /**
326
     * Get the collection persister instance.
327
     */
328 1121
    public function getCollectionPersister() : CollectionPersister
329
    {
330 1121
        if (! isset($this->collectionPersister)) {
331 1121
            $pb                        = $this->getPersistenceBuilder();
332 1121
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
333
        }
334 1121
        return $this->collectionPersister;
335
    }
336
337
    /**
338
     * Set the document persister instance to use for the given document name
339
     */
340 13
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
341
    {
342 13
        $this->persisters[$documentName] = $persister;
343 13
    }
344
345
    /**
346
     * Commits the UnitOfWork, executing all operations that have been postponed
347
     * up to this point. The state of all managed documents will be synchronized with
348
     * the database.
349
     *
350
     * The operations are executed in the following order:
351
     *
352
     * 1) All document insertions
353
     * 2) All document updates
354
     * 3) All document deletions
355
     *
356
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
357
     */
358 604
    public function commit(array $options = []) : void
359
    {
360
        // Raise preFlush
361 604
        if ($this->evm->hasListeners(Events::preFlush)) {
362
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
363
        }
364
365
        // Compute changes done since last commit.
366 604
        $this->computeChangeSets();
367
368 603
        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...
369 257
            $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...
370 215
            $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...
371 198
            $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...
372 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...
373 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...
374 603
            $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...
375
        ) {
376 22
            return; // Nothing to do.
377
        }
378
379 600
        $this->commitsInProgress++;
380 600
        if ($this->commitsInProgress > 1) {
381
            throw MongoDBException::commitInProgress();
382
        }
383
        try {
384 600
            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...
385 50
                foreach ($this->orphanRemovals as $removal) {
386 50
                    $this->remove($removal);
387
                }
388
            }
389
390
            // Raise onFlush
391 600
            if ($this->evm->hasListeners(Events::onFlush)) {
392 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
393
            }
394
395 599
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
396 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...
397 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...
398
            }
399
400 599
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
401 523
                [$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...
402 523
                $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...
403
            }
404
405 586
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
406 230
                [$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...
407 230
                $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...
408
            }
409
410 586
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
411 74
                [$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...
412 74
                $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...
413
            }
414
415
            // Raise postFlush
416 586
            if ($this->evm->hasListeners(Events::postFlush)) {
417
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
418
            }
419
420
            // Clear up
421 586
            $this->documentInsertions      =
422 586
            $this->documentUpserts         =
423 586
            $this->documentUpdates         =
424 586
            $this->documentDeletions       =
425 586
            $this->documentChangeSets      =
426 586
            $this->collectionUpdates       =
427 586
            $this->collectionDeletions     =
428 586
            $this->visitedCollections      =
429 586
            $this->scheduledForDirtyCheck  =
430 586
            $this->orphanRemovals          =
431 586
            $this->hasScheduledCollections = [];
432 586
        } finally {
433 600
            $this->commitsInProgress--;
434
        }
435 586
    }
436
437
    /**
438
     * Groups a list of scheduled documents by their class.
439
     */
440 599
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
441
    {
442 599
        if (empty($documents)) {
443 599
            return [];
444
        }
445 598
        $divided = [];
446 598
        $embeds  = [];
447 598
        foreach ($documents as $oid => $d) {
448 598
            $className = get_class($d);
449 598
            if (isset($embeds[$className])) {
450 74
                continue;
451
            }
452 598
            if (isset($divided[$className])) {
453 159
                $divided[$className][1][$oid] = $d;
454 159
                continue;
455
            }
456 598
            $class = $this->dm->getClassMetadata($className);
457 598
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
458 176
                $embeds[$className] = true;
459 176
                continue;
460
            }
461 598
            if (empty($divided[$class->name])) {
462 598
                $divided[$class->name] = [$class, [$oid => $d]];
463
            } else {
464 598
                $divided[$class->name][1][$oid] = $d;
465
            }
466
        }
467 598
        return $divided;
468
    }
469
470
    /**
471
     * Compute changesets of all documents scheduled for insertion.
472
     *
473
     * Embedded documents will not be processed.
474
     */
475 609
    private function computeScheduleInsertsChangeSets() : void
476
    {
477 609
        foreach ($this->documentInsertions as $document) {
478 536
            $class = $this->dm->getClassMetadata(get_class($document));
479 536
            if ($class->isEmbeddedDocument) {
480 158
                continue;
481
            }
482
483 530
            $this->computeChangeSet($class, $document);
484
        }
485 608
    }
486
487
    /**
488
     * Compute changesets of all documents scheduled for upsert.
489
     *
490
     * Embedded documents will not be processed.
491
     */
492 608
    private function computeScheduleUpsertsChangeSets() : void
493
    {
494 608
        foreach ($this->documentUpserts as $document) {
495 85
            $class = $this->dm->getClassMetadata(get_class($document));
496 85
            if ($class->isEmbeddedDocument) {
497
                continue;
498
            }
499
500 85
            $this->computeChangeSet($class, $document);
501
        }
502 608
    }
503
504
    /**
505
     * Gets the changeset for a document.
506
     *
507
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
508
     */
509 604
    public function getDocumentChangeSet(object $document) : array
510
    {
511 604
        $oid = spl_object_hash($document);
512
513 604
        return $this->documentChangeSets[$oid] ?? [];
514
    }
515
516
    /**
517
     * INTERNAL:
518
     * Sets the changeset for a document.
519
     */
520 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
521
    {
522 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
523 1
    }
524
525
    /**
526
     * Get a documents actual data, flattening all the objects to arrays.
527
     *
528
     * @return array
529
     */
530 609
    public function getDocumentActualData(object $document) : array
531
    {
532 609
        $class      = $this->dm->getClassMetadata(get_class($document));
533 609
        $actualData = [];
534 609
        foreach ($class->reflFields as $name => $refProp) {
535 609
            $mapping = $class->fieldMappings[$name];
536
            // skip not saved fields
537 609
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
538 197
                continue;
539
            }
540 609
            $value = $refProp->getValue($document);
541 609
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
542 609
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
543
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
544 393
                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...
545 146
                    $value = new ArrayCollection($value);
546
                }
547
548
                // Inject PersistentCollection
549 393
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
550 393
                $coll->setOwner($document, $mapping);
551 393
                $coll->setDirty(! $value->isEmpty());
552 393
                $class->reflFields[$name]->setValue($document, $coll);
553 393
                $actualData[$name] = $coll;
554
            } else {
555 609
                $actualData[$name] = $value;
556
            }
557
        }
558 609
        return $actualData;
559
    }
560
561
    /**
562
     * Computes the changes that happened to a single document.
563
     *
564
     * Modifies/populates the following properties:
565
     *
566
     * {@link originalDocumentData}
567
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
568
     * then it was not fetched from the database and therefore we have no original
569
     * document data yet. All of the current document data is stored as the original document data.
570
     *
571
     * {@link documentChangeSets}
572
     * The changes detected on all properties of the document are stored there.
573
     * A change is a tuple array where the first entry is the old value and the second
574
     * entry is the new value of the property. Changesets are used by persisters
575
     * to INSERT/UPDATE the persistent document state.
576
     *
577
     * {@link documentUpdates}
578
     * If the document is already fully MANAGED (has been fetched from the database before)
579
     * and any changes to its properties are detected, then a reference to the document is stored
580
     * there to mark it for an update.
581
     */
582 605
    public function computeChangeSet(ClassMetadata $class, object $document) : void
583
    {
584 605
        if (! $class->isInheritanceTypeNone()) {
585 183
            $class = $this->dm->getClassMetadata(get_class($document));
586
        }
587
588
        // Fire PreFlush lifecycle callbacks
589 605
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
590 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
591
        }
592
593 605
        $this->computeOrRecomputeChangeSet($class, $document);
594 604
    }
595
596
    /**
597
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
598
     */
599 605
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
600
    {
601 605
        $oid           = spl_object_hash($document);
602 605
        $actualData    = $this->getDocumentActualData($document);
603 605
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
604 605
        if ($isNewDocument) {
605
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
606
            // These result in an INSERT.
607 604
            $this->originalDocumentData[$oid] = $actualData;
608 604
            $changeSet                        = [];
609 604
            foreach ($actualData as $propName => $actualValue) {
610
                /* At this PersistentCollection shouldn't be here, probably it
611
                 * was cloned and its ownership must be fixed
612
                 */
613 604
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
614
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
615
                    $actualValue           = $actualData[$propName];
616
                }
617
                // ignore inverse side of reference relationship
618 604
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
619 187
                    continue;
620
                }
621 604
                $changeSet[$propName] = [null, $actualValue];
622
            }
623 604
            $this->documentChangeSets[$oid] = $changeSet;
624
        } else {
625 291
            if ($class->isReadOnly) {
626 2
                return;
627
            }
628
            // Document is "fully" MANAGED: it was already fully persisted before
629
            // and we have a copy of the original data
630 289
            $originalData           = $this->originalDocumentData[$oid];
631 289
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
632 289
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
633 2
                $changeSet = $this->documentChangeSets[$oid];
634
            } else {
635 289
                $changeSet = [];
636
            }
637
638 289
            $gridFSMetadataProperty = null;
639
640 289
            if ($class->isFile) {
641
                try {
642 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
643 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
644 1
                } catch (MappingException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
645
                }
646
            }
647
648 289
            foreach ($actualData as $propName => $actualValue) {
649
                // skip not saved fields
650 289
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
651 289
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
652 4
                    continue;
653
                }
654
655 288
                $orgValue = $originalData[$propName] ?? null;
656
657
                // skip if value has not changed
658 288
                if ($orgValue === $actualValue) {
659 286
                    if (! $actualValue instanceof PersistentCollectionInterface) {
660 286
                        continue;
661
                    }
662
663 200
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
664
                        // consider dirty collections as changed as well
665 176
                        continue;
666
                    }
667
                }
668
669
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
670 248
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
671 14
                    if ($orgValue !== null) {
672 8
                        $this->scheduleOrphanRemoval($orgValue);
673
                    }
674 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
675 14
                    continue;
676
                }
677
678
                // if owning side of reference-one relationship
679 241
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
680 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
681 1
                        $this->scheduleOrphanRemoval($orgValue);
682
                    }
683
684 13
                    $changeSet[$propName] = [$orgValue, $actualValue];
685 13
                    continue;
686
                }
687
688 234
                if ($isChangeTrackingNotify) {
689 3
                    continue;
690
                }
691
692
                // ignore inverse side of reference relationship
693 232
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
694 6
                    continue;
695
                }
696
697
                // Persistent collection was exchanged with the "originally"
698
                // created one. This can only mean it was cloned and replaced
699
                // on another document.
700 230
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
701 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
702
                }
703
704
                // if embed-many or reference-many relationship
705 230
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
706 119
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
                    /* If original collection was exchanged with a non-empty value
708
                     * and $set will be issued, there is no need to $unset it first
709
                     */
710 119
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
711 27
                        continue;
712
                    }
713 100
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
714 18
                        $this->scheduleCollectionDeletion($orgValue);
715
                    }
716 100
                    continue;
717
                }
718
719
                // skip equivalent date values
720 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
721
                    /** @var DateType $dateType */
722 37
                    $dateType      = Type::getType('date');
723 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
724 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
725
726 37
                    $orgTimestamp    = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
728
729 37
                    if ($orgTimestamp === $actualTimestamp) {
730 30
                        continue;
731
                    }
732
                }
733
734
                // regular field
735 131
                $changeSet[$propName] = [$orgValue, $actualValue];
736
            }
737 289
            if ($changeSet) {
738 237
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
739 19
                    ? $changeSet + $this->documentChangeSets[$oid]
740 234
                    : $changeSet;
741
742 237
                $this->originalDocumentData[$oid] = $actualData;
743 237
                $this->scheduleForUpdate($document);
744
            }
745
        }
746
747
        // Look for changes in associations of the document
748 605
        $associationMappings = array_filter(
749 605
            $class->associationMappings,
750
            static function ($assoc) {
751 464
                return empty($assoc['notSaved']);
752 605
            }
753
        );
754
755 605
        foreach ($associationMappings as $mapping) {
756 464
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
757
758 464
            if ($value === null) {
759 323
                continue;
760
            }
761
762 445
            $this->computeAssociationChanges($document, $mapping, $value);
763
764 444
            if (isset($mapping['reference'])) {
765 336
                continue;
766
            }
767
768 347
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
769
770 347
            foreach ($values as $obj) {
771 181
                $oid2 = spl_object_hash($obj);
772
773 181
                if (isset($this->documentChangeSets[$oid2])) {
774 179
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
775
                        // instance of $value is the same as it was previously otherwise there would be
776
                        // change set already in place
777 41
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
778
                    }
779
780 179
                    if (! $isNewDocument) {
781 81
                        $this->scheduleForUpdate($document);
782
                    }
783
784 347
                    break;
785
                }
786
            }
787
        }
788 604
    }
789
790
    /**
791
     * Computes all the changes that have been done to documents and collections
792
     * since the last commit and stores these changes in the _documentChangeSet map
793
     * temporarily for access by the persisters, until the UoW commit is finished.
794
     */
795 609
    public function computeChangeSets() : void
796
    {
797 609
        $this->computeScheduleInsertsChangeSets();
798 608
        $this->computeScheduleUpsertsChangeSets();
799
800
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
801 608
        foreach ($this->identityMap as $className => $documents) {
802 608
            $class = $this->dm->getClassMetadata($className);
803 608
            if ($class->isEmbeddedDocument) {
804
                /* we do not want to compute changes to embedded documents up front
805
                 * in case embedded document was replaced and its changeset
806
                 * would corrupt data. Embedded documents' change set will
807
                 * be calculated by reachability from owning document.
808
                 */
809 171
                continue;
810
            }
811
812
            // If change tracking is explicit or happens through notification, then only compute
813
            // changes on document of that type that are explicitly marked for synchronization.
814
            switch (true) {
815 608
                case $class->isChangeTrackingDeferredImplicit():
816 607
                    $documentsToProcess = $documents;
817 607
                    break;
818
819 4
                case isset($this->scheduledForDirtyCheck[$className]):
820 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
821 3
                    break;
822
823
                default:
824 4
                    $documentsToProcess = [];
825
            }
826
827 608
            foreach ($documentsToProcess as $document) {
828
                // Ignore uninitialized proxy objects
829 603
                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...
830 9
                    continue;
831
                }
832
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
833 603
                $oid = spl_object_hash($document);
834 603
                if (isset($this->documentInsertions[$oid])
835 332
                    || isset($this->documentUpserts[$oid])
836 286
                    || isset($this->documentDeletions[$oid])
837 603
                    || ! isset($this->documentStates[$oid])
838
                ) {
839 601
                    continue;
840
                }
841
842 608
                $this->computeChangeSet($class, $document);
843
            }
844
        }
845 608
    }
846
847
    /**
848
     * Computes the changes of an association.
849
     *
850
     * @param mixed $value The value of the association.
851
     *
852
     * @throws InvalidArgumentException
853
     */
854 445
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
855
    {
856 445
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
857 445
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
858 445
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
859
860 445
        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...
861 7
            return;
862
        }
863
864 444
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
865 251
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
866 247
                $this->scheduleCollectionUpdate($value);
867
            }
868
869 251
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
870 251
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
871 251
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
872 144
                $value->initialize();
873 144
                foreach ($value->getDeletedDocuments() as $orphan) {
874 23
                    $this->scheduleOrphanRemoval($orphan);
875
                }
876
            }
877
        }
878
879
        // Look through the documents, and in any of their associations,
880
        // for transient (new) documents, recursively. ("Persistence by reachability")
881
        // Unwrap. Uninitialized collections will simply be empty.
882 444
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
883
884 444
        $count = 0;
885 444
        foreach ($unwrappedValue as $key => $entry) {
886 360
            if (! is_object($entry)) {
887 1
                throw new InvalidArgumentException(
888 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
889
                );
890
            }
891
892 359
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
893
894 359
            $state = $this->getDocumentState($entry, self::STATE_NEW);
895
896
            // Handle "set" strategy for multi-level hierarchy
897 359
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
898 359
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
899
900 359
            $count++;
901
902
            switch ($state) {
903 359
                case self::STATE_NEW:
904 67
                    if (! $assoc['isCascadePersist']) {
905
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
906
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
907
                            . ' Explicitly persist the new document or configure cascading persist operations'
908
                            . ' on the relationship.');
909
                    }
910
911 67
                    $this->persistNew($targetClass, $entry);
912 67
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
913 67
                    $this->computeChangeSet($targetClass, $entry);
914 67
                    break;
915
916 355
                case self::STATE_MANAGED:
917 355
                    if ($targetClass->isEmbeddedDocument) {
918 172
                        [, $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...
919 172
                        if ($knownParent && $knownParent !== $parentDocument) {
920 6
                            $entry = clone $entry;
921 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
922 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
923 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
924 3
                                $poid = spl_object_hash($parentDocument);
925 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
926 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
927
                                }
928
                            } else {
929
                                // must use unwrapped value to not trigger orphan removal
930 4
                                $unwrappedValue[$key] = $entry;
931
                            }
932 6
                            $this->persistNew($targetClass, $entry);
933
                        }
934 172
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
935 172
                        $this->computeChangeSet($targetClass, $entry);
936
                    }
937 355
                    break;
938
939 1
                case self::STATE_REMOVED:
940
                    // Consume the $value as array (it's either an array or an ArrayAccess)
941
                    // and remove the element from Collection.
942 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
943
                        unset($value[$key]);
944
                    }
945 1
                    break;
946
947
                case self::STATE_DETACHED:
948
                    // Can actually not happen right now as we assume STATE_NEW,
949
                    // so the exception will be raised from the DBAL layer (constraint violation).
950
                    throw new InvalidArgumentException('A detached document was found through a '
951
                        . 'relationship during cascading a persist operation.');
952
953 359
                default:
954
                    // MANAGED associated documents are already taken into account
955
                    // during changeset calculation anyway, since they are in the identity map.
956
            }
957
        }
958 443
    }
959
960
    /**
961
     * INTERNAL:
962
     * Computes the changeset of an individual document, independently of the
963
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
964
     *
965
     * The passed document must be a managed document. If the document already has a change set
966
     * because this method is invoked during a commit cycle then the change sets are added.
967
     * whereby changes detected in this method prevail.
968
     *
969
     * @throws InvalidArgumentException If the passed document is not MANAGED.
970
     *
971
     * @ignore
972
     */
973 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
974
    {
975
        // Ignore uninitialized proxy objects
976 19
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
977 1
            return;
978
        }
979
980 18
        $oid = spl_object_hash($document);
981
982 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
983
            throw new InvalidArgumentException('Document must be managed.');
984
        }
985
986 18
        if (! $class->isInheritanceTypeNone()) {
987 2
            $class = $this->dm->getClassMetadata(get_class($document));
988
        }
989
990 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
991 18
    }
992
993
    /**
994
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
995
     */
996 634
    private function persistNew(ClassMetadata $class, object $document) : void
997
    {
998 634
        $this->lifecycleEventManager->prePersist($class, $document);
999 634
        $oid    = spl_object_hash($document);
1000 634
        $upsert = false;
1001 634
        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...
1002 634
            $idValue = $class->getIdentifierValue($document);
1003 634
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1004
1005 634
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1006 3
                throw new InvalidArgumentException(sprintf(
1007 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1008 3
                    get_class($document)
1009
                ));
1010
            }
1011
1012 633
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1013 1
                throw new InvalidArgumentException(sprintf(
1014 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1015 1
                    get_class($document)
1016
                ));
1017
            }
1018
1019 632
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1020 553
                $idValue = $class->idGenerator->generate($this->dm, $document);
1021 553
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1022 553
                $class->setIdentifierValue($document, $idValue);
1023
            }
1024
1025 632
            $this->documentIdentifiers[$oid] = $idValue;
1026
        } else {
1027
            // this is for embedded documents without identifiers
1028 151
            $this->documentIdentifiers[$oid] = $oid;
1029
        }
1030
1031 632
        $this->documentStates[$oid] = self::STATE_MANAGED;
1032
1033 632
        if ($upsert) {
1034 89
            $this->scheduleForUpsert($class, $document);
1035
        } else {
1036 562
            $this->scheduleForInsert($class, $document);
1037
        }
1038 632
    }
1039
1040
    /**
1041
     * Executes all document insertions for documents of the specified type.
1042
     */
1043 523
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1044
    {
1045 523
        $persister = $this->getDocumentPersister($class->name);
1046
1047 523
        foreach ($documents as $oid => $document) {
1048 523
            $persister->addInsert($document);
1049 523
            unset($this->documentInsertions[$oid]);
1050
        }
1051
1052 523
        $persister->executeInserts($options);
1053
1054 512
        foreach ($documents as $document) {
1055 512
            $this->lifecycleEventManager->postPersist($class, $document);
1056
        }
1057 512
    }
1058
1059
    /**
1060
     * Executes all document upserts for documents of the specified type.
1061
     */
1062 86
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1063
    {
1064 86
        $persister = $this->getDocumentPersister($class->name);
1065
1066 86
        foreach ($documents as $oid => $document) {
1067 86
            $persister->addUpsert($document);
1068 86
            unset($this->documentUpserts[$oid]);
1069
        }
1070
1071 86
        $persister->executeUpserts($options);
1072
1073 86
        foreach ($documents as $document) {
1074 86
            $this->lifecycleEventManager->postPersist($class, $document);
1075
        }
1076 86
    }
1077
1078
    /**
1079
     * Executes all document updates for documents of the specified type.
1080
     */
1081 230
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1082
    {
1083 230
        if ($class->isReadOnly) {
1084
            return;
1085
        }
1086
1087 230
        $className = $class->name;
1088 230
        $persister = $this->getDocumentPersister($className);
1089
1090 230
        foreach ($documents as $oid => $document) {
1091 230
            $this->lifecycleEventManager->preUpdate($class, $document);
1092
1093 230
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1094 229
                $persister->update($document, $options);
1095
            }
1096
1097 223
            unset($this->documentUpdates[$oid]);
1098
1099 223
            $this->lifecycleEventManager->postUpdate($class, $document);
1100
        }
1101 222
    }
1102
1103
    /**
1104
     * Executes all document deletions for documents of the specified type.
1105
     */
1106 74
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1107
    {
1108 74
        $persister = $this->getDocumentPersister($class->name);
1109
1110 74
        foreach ($documents as $oid => $document) {
1111 74
            if (! $class->isEmbeddedDocument) {
1112 36
                $persister->delete($document, $options);
1113
            }
1114
            unset(
1115 72
                $this->documentDeletions[$oid],
1116 72
                $this->documentIdentifiers[$oid],
1117 72
                $this->originalDocumentData[$oid]
1118
            );
1119
1120
            // Clear snapshot information for any referenced PersistentCollection
1121
            // http://www.doctrine-project.org/jira/browse/MODM-95
1122 72
            foreach ($class->associationMappings as $fieldMapping) {
1123 48
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1124 38
                    continue;
1125
                }
1126
1127 28
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1128 28
                if (! ($value instanceof PersistentCollectionInterface)) {
1129 7
                    continue;
1130
                }
1131
1132 24
                $value->clearSnapshot();
1133
            }
1134
1135
            // Document with this $oid after deletion treated as NEW, even if the $oid
1136
            // is obtained by a new document because the old one went out of scope.
1137 72
            $this->documentStates[$oid] = self::STATE_NEW;
1138
1139 72
            $this->lifecycleEventManager->postRemove($class, $document);
1140
        }
1141 72
    }
1142
1143
    /**
1144
     * Schedules a document for insertion into the database.
1145
     * If the document already has an identifier, it will be added to the
1146
     * identity map.
1147
     *
1148
     * @throws InvalidArgumentException
1149
     */
1150 565
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1151
    {
1152 565
        $oid = spl_object_hash($document);
1153
1154 565
        if (isset($this->documentUpdates[$oid])) {
1155
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1156
        }
1157 565
        if (isset($this->documentDeletions[$oid])) {
1158
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1159
        }
1160 565
        if (isset($this->documentInsertions[$oid])) {
1161
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1162
        }
1163
1164 565
        $this->documentInsertions[$oid] = $document;
1165
1166 565
        if (! isset($this->documentIdentifiers[$oid])) {
1167 3
            return;
1168
        }
1169
1170 562
        $this->addToIdentityMap($document);
1171 562
    }
1172
1173
    /**
1174
     * Schedules a document for upsert into the database and adds it to the
1175
     * identity map
1176
     *
1177
     * @throws InvalidArgumentException
1178
     */
1179 92
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1180
    {
1181 92
        $oid = spl_object_hash($document);
1182
1183 92
        if ($class->isEmbeddedDocument) {
1184
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1185
        }
1186 92
        if (isset($this->documentUpdates[$oid])) {
1187
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1188
        }
1189 92
        if (isset($this->documentDeletions[$oid])) {
1190
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1191
        }
1192 92
        if (isset($this->documentUpserts[$oid])) {
1193
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1194
        }
1195
1196 92
        $this->documentUpserts[$oid]     = $document;
1197 92
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1198 92
        $this->addToIdentityMap($document);
1199 92
    }
1200
1201
    /**
1202
     * Checks whether a document is scheduled for insertion.
1203
     */
1204 104
    public function isScheduledForInsert(object $document) : bool
1205
    {
1206 104
        return isset($this->documentInsertions[spl_object_hash($document)]);
1207
    }
1208
1209
    /**
1210
     * Checks whether a document is scheduled for upsert.
1211
     */
1212 5
    public function isScheduledForUpsert(object $document) : bool
1213
    {
1214 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1215
    }
1216
1217
    /**
1218
     * Schedules a document for being updated.
1219
     *
1220
     * @throws InvalidArgumentException
1221
     */
1222 238
    public function scheduleForUpdate(object $document) : void
1223
    {
1224 238
        $oid = spl_object_hash($document);
1225 238
        if (! isset($this->documentIdentifiers[$oid])) {
1226
            throw new InvalidArgumentException('Document has no identity.');
1227
        }
1228
1229 238
        if (isset($this->documentDeletions[$oid])) {
1230
            throw new InvalidArgumentException('Document is removed.');
1231
        }
1232
1233 238
        if (isset($this->documentUpdates[$oid])
1234 238
            || isset($this->documentInsertions[$oid])
1235 238
            || isset($this->documentUpserts[$oid])) {
1236 98
            return;
1237
        }
1238
1239 236
        $this->documentUpdates[$oid] = $document;
1240 236
    }
1241
1242
    /**
1243
     * Checks whether a document is registered as dirty in the unit of work.
1244
     * Note: Is not very useful currently as dirty documents are only registered
1245
     * at commit time.
1246
     */
1247 21
    public function isScheduledForUpdate(object $document) : bool
1248
    {
1249 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1250
    }
1251
1252 1
    public function isScheduledForDirtyCheck(object $document) : bool
1253
    {
1254 1
        $class = $this->dm->getClassMetadata(get_class($document));
1255 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1256
    }
1257
1258
    /**
1259
     * INTERNAL:
1260
     * Schedules a document for deletion.
1261
     */
1262 79
    public function scheduleForDelete(object $document) : void
1263
    {
1264 79
        $oid = spl_object_hash($document);
1265
1266 79
        if (isset($this->documentInsertions[$oid])) {
1267 2
            if ($this->isInIdentityMap($document)) {
1268 2
                $this->removeFromIdentityMap($document);
1269
            }
1270 2
            unset($this->documentInsertions[$oid]);
1271 2
            return; // document has not been persisted yet, so nothing more to do.
1272
        }
1273
1274 78
        if (! $this->isInIdentityMap($document)) {
1275 2
            return; // ignore
1276
        }
1277
1278 77
        $this->removeFromIdentityMap($document);
1279 77
        $this->documentStates[$oid] = self::STATE_REMOVED;
1280
1281 77
        if (isset($this->documentUpdates[$oid])) {
1282
            unset($this->documentUpdates[$oid]);
1283
        }
1284 77
        if (isset($this->documentDeletions[$oid])) {
1285
            return;
1286
        }
1287
1288 77
        $this->documentDeletions[$oid] = $document;
1289 77
    }
1290
1291
    /**
1292
     * Checks whether a document is registered as removed/deleted with the unit
1293
     * of work.
1294
     */
1295 5
    public function isScheduledForDelete(object $document) : bool
1296
    {
1297 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1298
    }
1299
1300
    /**
1301
     * Checks whether a document is scheduled for insertion, update or deletion.
1302
     */
1303 250
    public function isDocumentScheduled(object $document) : bool
1304
    {
1305 250
        $oid = spl_object_hash($document);
1306 250
        return isset($this->documentInsertions[$oid]) ||
1307 132
            isset($this->documentUpserts[$oid]) ||
1308 122
            isset($this->documentUpdates[$oid]) ||
1309 250
            isset($this->documentDeletions[$oid]);
1310
    }
1311
1312
    /**
1313
     * INTERNAL:
1314
     * Registers a document in the identity map.
1315
     *
1316
     * Note that documents in a hierarchy are registered with the class name of
1317
     * the root document. Identifiers are serialized before being used as array
1318
     * keys to allow differentiation of equal, but not identical, values.
1319
     *
1320
     * @ignore
1321
     */
1322 673
    public function addToIdentityMap(object $document) : bool
1323
    {
1324 673
        $class = $this->dm->getClassMetadata(get_class($document));
1325 673
        $id    = $this->getIdForIdentityMap($document);
1326
1327 673
        if (isset($this->identityMap[$class->name][$id])) {
1328 44
            return false;
1329
        }
1330
1331 673
        $this->identityMap[$class->name][$id] = $document;
1332
1333 673
        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...
1334 673
            ( ! $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...
1335 3
            $document->addPropertyChangedListener($this);
1336
        }
1337
1338 673
        return true;
1339
    }
1340
1341
    /**
1342
     * Gets the state of a document with regard to the current unit of work.
1343
     *
1344
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1345
     *                         This parameter can be set to improve performance of document state detection
1346
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1347
     *                         is either known or does not matter for the caller of the method.
1348
     */
1349 638
    public function getDocumentState(object $document, ?int $assume = null) : int
1350
    {
1351 638
        $oid = spl_object_hash($document);
1352
1353 638
        if (isset($this->documentStates[$oid])) {
1354 399
            return $this->documentStates[$oid];
1355
        }
1356
1357 637
        $class = $this->dm->getClassMetadata(get_class($document));
1358
1359 637
        if ($class->isEmbeddedDocument) {
1360 185
            return self::STATE_NEW;
1361
        }
1362
1363 634
        if ($assume !== null) {
1364 632
            return $assume;
1365
        }
1366
1367
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1368
         * known. Note that you cannot remember the NEW or DETACHED state in
1369
         * _documentStates since the UoW does not hold references to such
1370
         * objects and the object hash can be reused. More generally, because
1371
         * the state may "change" between NEW/DETACHED without the UoW being
1372
         * aware of it.
1373
         */
1374 3
        $id = $class->getIdentifierObject($document);
1375
1376 3
        if ($id === null) {
1377 2
            return self::STATE_NEW;
1378
        }
1379
1380
        // Check for a version field, if available, to avoid a DB lookup.
1381 2
        if ($class->isVersioned && $class->versionField !== null) {
1382
            return $class->getFieldValue($document, $class->versionField)
1383
                ? self::STATE_DETACHED
1384
                : self::STATE_NEW;
1385
        }
1386
1387
        // Last try before DB lookup: check the identity map.
1388 2
        if ($this->tryGetById($id, $class)) {
1389 1
            return self::STATE_DETACHED;
1390
        }
1391
1392
        // DB lookup
1393 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1394 1
            return self::STATE_DETACHED;
1395
        }
1396
1397 1
        return self::STATE_NEW;
1398
    }
1399
1400
    /**
1401
     * INTERNAL:
1402
     * Removes a document from the identity map. This effectively detaches the
1403
     * document from the persistence management of Doctrine.
1404
     *
1405
     * @throws InvalidArgumentException
1406
     *
1407
     * @ignore
1408
     */
1409 91
    public function removeFromIdentityMap(object $document) : bool
1410
    {
1411 91
        $oid = spl_object_hash($document);
1412
1413
        // Check if id is registered first
1414 91
        if (! isset($this->documentIdentifiers[$oid])) {
1415
            return false;
1416
        }
1417
1418 91
        $class = $this->dm->getClassMetadata(get_class($document));
1419 91
        $id    = $this->getIdForIdentityMap($document);
1420
1421 91
        if (isset($this->identityMap[$class->name][$id])) {
1422 91
            unset($this->identityMap[$class->name][$id]);
1423 91
            $this->documentStates[$oid] = self::STATE_DETACHED;
1424 91
            return true;
1425
        }
1426
1427
        return false;
1428
    }
1429
1430
    /**
1431
     * INTERNAL:
1432
     * Gets a document in the identity map by its identifier hash.
1433
     *
1434
     * @param mixed $id Document identifier
1435
     *
1436
     * @throws InvalidArgumentException If the class does not have an identifier.
1437
     *
1438
     * @ignore
1439
     */
1440 37
    public function getById($id, ClassMetadata $class) : object
1441
    {
1442 37
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1443
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1444
        }
1445
1446 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1447
1448 37
        return $this->identityMap[$class->name][$serializedId];
1449
    }
1450
1451
    /**
1452
     * INTERNAL:
1453
     * Tries to get a document by its identifier hash. If no document is found
1454
     * for the given hash, FALSE is returned.
1455
     *
1456
     * @param mixed $id Document identifier
1457
     *
1458
     * @return mixed The found document or FALSE.
1459
     *
1460
     * @throws InvalidArgumentException If the class does not have an identifier.
1461
     *
1462
     * @ignore
1463
     */
1464 306
    public function tryGetById($id, ClassMetadata $class)
1465
    {
1466 306
        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...
1467
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1468
        }
1469
1470 306
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1471
1472 306
        return $this->identityMap[$class->name][$serializedId] ?? false;
1473
    }
1474
1475
    /**
1476
     * Schedules a document for dirty-checking at commit-time.
1477
     *
1478
     * @todo Rename: scheduleForSynchronization
1479
     */
1480 3
    public function scheduleForDirtyCheck(object $document) : void
1481
    {
1482 3
        $class                                                                  = $this->dm->getClassMetadata(get_class($document));
1483 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1484 3
    }
1485
1486
    /**
1487
     * Checks whether a document is registered in the identity map.
1488
     */
1489 87
    public function isInIdentityMap(object $document) : bool
1490
    {
1491 87
        $oid = spl_object_hash($document);
1492
1493 87
        if (! isset($this->documentIdentifiers[$oid])) {
1494 6
            return false;
1495
        }
1496
1497 85
        $class = $this->dm->getClassMetadata(get_class($document));
1498 85
        $id    = $this->getIdForIdentityMap($document);
1499
1500 85
        return isset($this->identityMap[$class->name][$id]);
1501
    }
1502
1503 673
    private function getIdForIdentityMap(object $document) : string
1504
    {
1505 673
        $class = $this->dm->getClassMetadata(get_class($document));
1506
1507 673
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
1748
                            // Do not merge fields marked lazy that have not been fetched
1749
                            continue;
1750 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1751
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1752
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1753
                                /** @var ClassMetadata $targetClass */
1754
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1755
                                $relatedId   = $targetClass->getIdentifierObject($other);
1756
1757
                                if ($targetClass->subClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $targetClass->subClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2005 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2006
                    // Collections are the same, so there is nothing to do
2007 1
                    continue;
2008
                }
2009
2010 8
                foreach ($relatedDocuments as $relatedDocument) {
2011 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2012
                }
2013 6
            } elseif ($relatedDocuments !== null) {
2014 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2015
            }
2016
        }
2017 11
    }
2018
2019
    /**
2020
     * Cascades the save operation to associated documents.
2021
     */
2022 631
    private function cascadePersist(object $document, array &$visited) : void
2023
    {
2024 631
        $class = $this->dm->getClassMetadata(get_class($document));
2025
2026 631
        $associationMappings = array_filter(
2027 631
            $class->associationMappings,
2028
            static function ($assoc) {
2029 486
                return $assoc['isCascadePersist'];
2030 631
            }
2031
        );
2032
2033 631
        foreach ($associationMappings as $fieldName => $mapping) {
2034 434
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2035
2036 434
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2037 363
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2038 12
                    if ($relatedDocuments->getOwner() !== $document) {
2039 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2040
                    }
2041
                    // Unwrap so that foreach() does not initialize
2042 12
                    $relatedDocuments = $relatedDocuments->unwrap();
2043
                }
2044
2045 363
                $count = 0;
2046 363
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2047 194
                    if (! empty($mapping['embedded'])) {
2048 123
                        [, $knownParent ] = $this->getParentAssociation($relatedDocument);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2524 16
                    $document->addPropertyChangedListener($this);
2525
                }
2526
            } else {
2527 85
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2528
            }
2529 95
            if ($overrideLocalValues) {
2530 42
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2531 95
                $this->originalDocumentData[$oid] = $data;
2532
            }
2533
        } else {
2534 352
            if ($document === null) {
2535 352
                $document = $class->newInstance();
2536
            }
2537
2538 352
            if (! $class->isQueryResultDocument) {
2539 348
                $this->registerManaged($document, $id, $data);
2540 348
                $oid                                            = spl_object_hash($document);
2541 348
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2542 348
                $this->identityMap[$class->name][$serializedId] = $document;
2543
            }
2544
2545 352
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2546
2547 352
            if (! $class->isQueryResultDocument) {
2548 348
                $this->originalDocumentData[$oid] = $data;
2549
            }
2550
        }
2551
2552 395
        return $document;
2553
    }
2554
2555
    /**
2556
     * Initializes (loads) an uninitialized persistent collection of a document.
2557
     */
2558 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
2559
    {
2560 178
        if ($collection->getOwner() === null) {
2561
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2562
        }
2563
2564 178
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2565 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2566 178
    }
2567
2568
    /**
2569
     * Gets the identity map of the UnitOfWork.
2570
     */
2571
    public function getIdentityMap() : array
2572
    {
2573
        return $this->identityMap;
2574
    }
2575
2576
    /**
2577
     * Gets the original data of a document. The original data is the data that was
2578
     * present at the time the document was reconstituted from the database.
2579
     *
2580
     * @return array
2581
     */
2582 1
    public function getOriginalDocumentData(object $document) : array
2583
    {
2584 1
        $oid = spl_object_hash($document);
2585
2586 1
        return $this->originalDocumentData[$oid] ?? [];
2587
    }
2588
2589 60
    public function setOriginalDocumentData(object $document, array $data) : void
2590
    {
2591 60
        $oid                              = spl_object_hash($document);
2592 60
        $this->originalDocumentData[$oid] = $data;
2593 60
        unset($this->documentChangeSets[$oid]);
2594 60
    }
2595
2596
    /**
2597
     * INTERNAL:
2598
     * Sets a property value of the original data array of a document.
2599
     *
2600
     * @param mixed $value
2601
     *
2602
     * @ignore
2603
     */
2604 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2605
    {
2606 3
        $this->originalDocumentData[$oid][$property] = $value;
2607 3
    }
2608
2609
    /**
2610
     * Gets the identifier of a document.
2611
     *
2612
     * @return mixed The identifier value
2613
     */
2614 445
    public function getDocumentIdentifier(object $document)
2615
    {
2616 445
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2617
    }
2618
2619
    /**
2620
     * Checks whether the UnitOfWork has any pending insertions.
2621
     *
2622
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2623
     */
2624
    public function hasPendingInsertions() : bool
2625
    {
2626
        return ! empty($this->documentInsertions);
2627
    }
2628
2629
    /**
2630
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2631
     * number of documents in the identity map.
2632
     */
2633 2
    public function size() : int
2634
    {
2635 2
        $count = 0;
2636 2
        foreach ($this->identityMap as $documentSet) {
2637 2
            $count += count($documentSet);
2638
        }
2639 2
        return $count;
2640
    }
2641
2642
    /**
2643
     * INTERNAL:
2644
     * Registers a document as managed.
2645
     *
2646
     * TODO: This method assumes that $id is a valid PHP identifier for the
2647
     * document class. If the class expects its database identifier to be an
2648
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2649
     * document identifiers map will become inconsistent with the identity map.
2650
     * In the future, we may want to round-trip $id through a PHP and database
2651
     * conversion and throw an exception if it's inconsistent.
2652
     *
2653
     * @param mixed $id The identifier values.
2654
     */
2655 381
    public function registerManaged(object $document, $id, array $data) : void
2656
    {
2657 381
        $oid   = spl_object_hash($document);
2658 381
        $class = $this->dm->getClassMetadata(get_class($document));
2659
2660 381
        if (! $class->identifier || $id === null) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2661 110
            $this->documentIdentifiers[$oid] = $oid;
2662
        } else {
2663 375
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2664
        }
2665
2666 381
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2667 381
        $this->originalDocumentData[$oid] = $data;
2668 381
        $this->addToIdentityMap($document);
2669 381
    }
2670
2671
    /**
2672
     * INTERNAL:
2673
     * Clears the property changeset of the document with the given OID.
2674
     */
2675
    public function clearDocumentChangeSet(string $oid)
2676
    {
2677
        $this->documentChangeSets[$oid] = [];
2678
    }
2679
2680
    /* PropertyChangedListener implementation */
2681
2682
    /**
2683
     * Notifies this UnitOfWork of a property change in a document.
2684
     *
2685
     * @param object $document     The document that owns the property.
2686
     * @param string $propertyName The name of the property that changed.
2687
     * @param mixed  $oldValue     The old value of the property.
2688
     * @param mixed  $newValue     The new value of the property.
2689
     */
2690 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2691
    {
2692 2
        $oid   = spl_object_hash($document);
2693 2
        $class = $this->dm->getClassMetadata(get_class($document));
2694
2695 2
        if (! isset($class->fieldMappings[$propertyName])) {
2696 1
            return; // ignore non-persistent fields
2697
        }
2698
2699
        // Update changeset and mark document for synchronization
2700 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2701 2
        if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2702
            return;
2703
        }
2704
2705 2
        $this->scheduleForDirtyCheck($document);
2706 2
    }
2707
2708
    /**
2709
     * Gets the currently scheduled document insertions in this UnitOfWork.
2710
     */
2711 3
    public function getScheduledDocumentInsertions() : array
2712
    {
2713 3
        return $this->documentInsertions;
2714
    }
2715
2716
    /**
2717
     * Gets the currently scheduled document upserts in this UnitOfWork.
2718
     */
2719 1
    public function getScheduledDocumentUpserts() : array
2720
    {
2721 1
        return $this->documentUpserts;
2722
    }
2723
2724
    /**
2725
     * Gets the currently scheduled document updates in this UnitOfWork.
2726
     */
2727 2
    public function getScheduledDocumentUpdates() : array
2728
    {
2729 2
        return $this->documentUpdates;
2730
    }
2731
2732
    /**
2733
     * Gets the currently scheduled document deletions in this UnitOfWork.
2734
     */
2735
    public function getScheduledDocumentDeletions() : array
2736
    {
2737
        return $this->documentDeletions;
2738
    }
2739
2740
    /**
2741
     * Get the currently scheduled complete collection deletions
2742
     */
2743
    public function getScheduledCollectionDeletions() : array
2744
    {
2745
        return $this->collectionDeletions;
2746
    }
2747
2748
    /**
2749
     * Gets the currently scheduled collection inserts, updates and deletes.
2750
     */
2751
    public function getScheduledCollectionUpdates() : array
2752
    {
2753
        return $this->collectionUpdates;
2754
    }
2755
2756
    /**
2757
     * Helper method to initialize a lazy loading proxy or persistent collection.
2758
     */
2759
    public function initializeObject(object $obj) : void
2760
    {
2761
        if ($obj instanceof GhostObjectInterface) {
0 ignored issues
show
Bug introduced by
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
2762
            $obj->initializeProxy();
2763
        } elseif ($obj instanceof PersistentCollectionInterface) {
2764
            $obj->initialize();
2765
        }
2766
    }
2767
2768
    private function objToStr(object $obj) : string
2769
    {
2770
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2771
    }
2772
}
2773