Completed
Push — master ( 26ecbc...8c0c5d )
by Maciej
14s
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 1649
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
269
    {
270 1649
        $this->dm                    = $dm;
271 1649
        $this->evm                   = $evm;
272 1649
        $this->hydratorFactory       = $hydratorFactory;
273 1649
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
274 1649
    }
275
276
    /**
277
     * Factory for returning new PersistenceBuilder instances used for preparing data into
278
     * queries for insert persistence.
279
     */
280 1130
    public function getPersistenceBuilder() : PersistenceBuilder
281
    {
282 1130
        if (! $this->persistenceBuilder) {
283 1130
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
284
        }
285 1130
        return $this->persistenceBuilder;
286
    }
287
288
    /**
289
     * Sets the parent association for a given embedded document.
290
     */
291 209
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
292
    {
293 209
        $oid                                   = spl_object_hash($document);
294 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
295 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
296 209
    }
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 233
    public function getParentAssociation(object $document) : ?array
306
    {
307 233
        $oid = spl_object_hash($document);
308
309 233
        return $this->parentAssociations[$oid] ?? null;
310
    }
311
312
    /**
313
     * Get the document persister instance for the given document name
314
     */
315 1128
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
316
    {
317 1128
        if (! isset($this->persisters[$documentName])) {
318 1115
            $class                           = $this->dm->getClassMetadata($documentName);
319 1115
            $pb                              = $this->getPersistenceBuilder();
320 1115
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
321
        }
322 1128
        return $this->persisters[$documentName];
323
    }
324
325
    /**
326
     * Get the collection persister instance.
327
     */
328 1128
    public function getCollectionPersister() : CollectionPersister
329
    {
330 1128
        if (! isset($this->collectionPersister)) {
331 1128
            $pb                        = $this->getPersistenceBuilder();
332 1128
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
333
        }
334 1128
        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 611
    public function commit(array $options = []) : void
359
    {
360
        // Raise preFlush
361 611
        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 611
        $this->computeChangeSets();
367
368 610
        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 610
            $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 607
        $this->commitsInProgress++;
380 607
        if ($this->commitsInProgress > 1) {
381
            throw MongoDBException::commitInProgress();
382
        }
383
        try {
384 607
            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 55
                foreach ($this->orphanRemovals as $removal) {
386 55
                    $this->remove($removal);
387
                }
388
            }
389
390
            // Raise onFlush
391 607
            if ($this->evm->hasListeners(Events::onFlush)) {
392 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
393
            }
394
395 606
            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 606
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
401 530
                [$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 530
                $this->executeInserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
403
            }
404
405 593
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
406 236
                [$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 236
                $this->executeUpdates($class, $documents, $options);
0 ignored issues
show
Bug introduced by
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

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

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

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

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

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

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

Loading history...
413
            }
414
415
            // Raise postFlush
416 593
            if ($this->evm->hasListeners(Events::postFlush)) {
417
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
418
            }
419
420
            // Clear up
421 593
            $this->documentInsertions      =
422 593
            $this->documentUpserts         =
423 593
            $this->documentUpdates         =
424 593
            $this->documentDeletions       =
425 593
            $this->documentChangeSets      =
426 593
            $this->collectionUpdates       =
427 593
            $this->collectionDeletions     =
428 593
            $this->visitedCollections      =
429 593
            $this->scheduledForDirtyCheck  =
430 593
            $this->orphanRemovals          =
431 593
            $this->hasScheduledCollections = [];
432 593
        } finally {
433 607
            $this->commitsInProgress--;
434
        }
435 593
    }
436
437
    /**
438
     * Groups a list of scheduled documents by their class.
439
     */
440 606
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
441
    {
442 606
        if (empty($documents)) {
443 606
            return [];
444
        }
445 605
        $divided = [];
446 605
        $embeds  = [];
447 605
        foreach ($documents as $oid => $d) {
448 605
            $className = get_class($d);
449 605
            if (isset($embeds[$className])) {
450 81
                continue;
451
            }
452 605
            if (isset($divided[$className])) {
453 165
                $divided[$className][1][$oid] = $d;
454 165
                continue;
455
            }
456 605
            $class = $this->dm->getClassMetadata($className);
457 605
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
458 183
                $embeds[$className] = true;
459 183
                continue;
460
            }
461 605
            if (empty($divided[$class->name])) {
462 605
                $divided[$class->name] = [$class, [$oid => $d]];
463
            } else {
464 605
                $divided[$class->name][1][$oid] = $d;
465
            }
466
        }
467 605
        return $divided;
468
    }
469
470
    /**
471
     * Compute changesets of all documents scheduled for insertion.
472
     *
473
     * Embedded documents will not be processed.
474
     */
475 616
    private function computeScheduleInsertsChangeSets() : void
476
    {
477 616
        foreach ($this->documentInsertions as $document) {
478 543
            $class = $this->dm->getClassMetadata(get_class($document));
479 543
            if ($class->isEmbeddedDocument) {
480 165
                continue;
481
            }
482
483 537
            $this->computeChangeSet($class, $document);
484
        }
485 615
    }
486
487
    /**
488
     * Compute changesets of all documents scheduled for upsert.
489
     *
490
     * Embedded documents will not be processed.
491
     */
492 615
    private function computeScheduleUpsertsChangeSets() : void
493
    {
494 615
        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 615
    }
503
504
    /**
505
     * Gets the changeset for a document.
506
     *
507
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
508
     */
509 611
    public function getDocumentChangeSet(object $document) : array
510
    {
511 611
        $oid = spl_object_hash($document);
512
513 611
        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 616
    public function getDocumentActualData(object $document) : array
531
    {
532 616
        $class      = $this->dm->getClassMetadata(get_class($document));
533 616
        $actualData = [];
534 616
        foreach ($class->reflFields as $name => $refProp) {
535 616
            $mapping = $class->fieldMappings[$name];
536
            // skip not saved fields
537 616
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
538 197
                continue;
539
            }
540 616
            $value = $refProp->getValue($document);
541 616
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
542 616
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
543
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
544 400
                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 147
                    $value = new ArrayCollection($value);
546
                }
547
548
                // Inject PersistentCollection
549 400
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
550 400
                $coll->setOwner($document, $mapping);
551 400
                $coll->setDirty(! $value->isEmpty());
552 400
                $class->reflFields[$name]->setValue($document, $coll);
553 400
                $actualData[$name] = $coll;
554
            } else {
555 616
                $actualData[$name] = $value;
556
            }
557
        }
558 616
        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 612
    public function computeChangeSet(ClassMetadata $class, object $document) : void
583
    {
584 612
        if (! $class->isInheritanceTypeNone()) {
585 183
            $class = $this->dm->getClassMetadata(get_class($document));
586
        }
587
588
        // Fire PreFlush lifecycle callbacks
589 612
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
590 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
591
        }
592
593 612
        $this->computeOrRecomputeChangeSet($class, $document);
594 611
    }
595
596
    /**
597
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
598
     */
599 612
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
600
    {
601 612
        $oid           = spl_object_hash($document);
602 612
        $actualData    = $this->getDocumentActualData($document);
603 612
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
604 612
        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 611
            $this->originalDocumentData[$oid] = $actualData;
608 611
            $changeSet                        = [];
609 611
            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 611
                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 611
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
619 187
                    continue;
620
                }
621 611
                $changeSet[$propName] = [null, $actualValue];
622
            }
623 611
            $this->documentChangeSets[$oid] = $changeSet;
624
        } else {
625 297
            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 295
            $originalData           = $this->originalDocumentData[$oid];
631 295
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
632 295
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
633 2
                $changeSet = $this->documentChangeSets[$oid];
634
            } else {
635 295
                $changeSet = [];
636
            }
637
638 295
            $gridFSMetadataProperty = null;
639
640 295
            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 295
            foreach ($actualData as $propName => $actualValue) {
649
                // skip not saved fields
650 295
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
651 295
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
652 4
                    continue;
653
                }
654
655 294
                $orgValue = $originalData[$propName] ?? null;
656
657
                // skip if value has not changed
658 294
                if ($orgValue === $actualValue) {
659 292
                    if (! $actualValue instanceof PersistentCollectionInterface) {
660 292
                        continue;
661
                    }
662
663 206
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
664
                        // consider dirty collections as changed as well
665 182
                        continue;
666
                    }
667
                }
668
669
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
670 254
                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 247
                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 240
                if ($isChangeTrackingNotify) {
689 3
                    continue;
690
                }
691
692
                // ignore inverse side of reference relationship
693 238
                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 236
                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 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
706 125
                    $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 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
711 31
                        continue;
712
                    }
713 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
714 18
                        $this->scheduleCollectionDeletion($orgValue);
715
                    }
716 104
                    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 295
            if ($changeSet) {
738 243
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
739 19
                    ? $changeSet + $this->documentChangeSets[$oid]
740 240
                    : $changeSet;
741
742 243
                $this->originalDocumentData[$oid] = $actualData;
743 243
                $this->scheduleForUpdate($document);
744
            }
745
        }
746
747
        // Look for changes in associations of the document
748 612
        $associationMappings = array_filter(
749 612
            $class->associationMappings,
750
            static function ($assoc) {
751 471
                return empty($assoc['notSaved']);
752 612
            }
753
        );
754
755 612
        foreach ($associationMappings as $mapping) {
756 471
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
757
758 471
            if ($value === null) {
759 323
                continue;
760
            }
761
762 452
            $this->computeAssociationChanges($document, $mapping, $value);
763
764 451
            if (isset($mapping['reference'])) {
765 337
                continue;
766
            }
767
768 354
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
769
770 354
            foreach ($values as $obj) {
771 188
                $oid2 = spl_object_hash($obj);
772
773 188
                if (isset($this->documentChangeSets[$oid2])) {
774 186
                    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 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
778
                    }
779
780 186
                    if (! $isNewDocument) {
781 87
                        $this->scheduleForUpdate($document);
782
                    }
783
784 354
                    break;
785
                }
786
            }
787
        }
788 611
    }
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 616
    public function computeChangeSets() : void
796
    {
797 616
        $this->computeScheduleInsertsChangeSets();
798 615
        $this->computeScheduleUpsertsChangeSets();
799
800
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
801 615
        foreach ($this->identityMap as $className => $documents) {
802 615
            $class = $this->dm->getClassMetadata($className);
803 615
            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 178
                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 615
                case $class->isChangeTrackingDeferredImplicit():
816 614
                    $documentsToProcess = $documents;
817 614
                    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 615
            foreach ($documentsToProcess as $document) {
828
                // Ignore uninitialized proxy objects
829 610
                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 610
                $oid = spl_object_hash($document);
834 610
                if (isset($this->documentInsertions[$oid])
835 338
                    || isset($this->documentUpserts[$oid])
836 292
                    || isset($this->documentDeletions[$oid])
837 610
                    || ! isset($this->documentStates[$oid])
838
                ) {
839 608
                    continue;
840
                }
841
842 615
                $this->computeChangeSet($class, $document);
843
            }
844
        }
845 615
    }
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 452
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
855
    {
856 452
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
857 452
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
858 452
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
859
860 452
        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 451
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
865 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
866 254
                $this->scheduleCollectionUpdate($value);
867
            }
868
869 258
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
870 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
871 258
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
872 151
                $value->initialize();
873 151
                foreach ($value->getDeletedDocuments() as $orphan) {
874 25
                    $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 451
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
883
884 451
        $count = 0;
885 451
        foreach ($unwrappedValue as $key => $entry) {
886 367
            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 366
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
893
894 366
            $state = $this->getDocumentState($entry, self::STATE_NEW);
895
896
            // Handle "set" strategy for multi-level hierarchy
897 366
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
898 366
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
899
900 366
            $count++;
901
902
            switch ($state) {
903 366
                case self::STATE_NEW:
904 70
                    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 70
                    $this->persistNew($targetClass, $entry);
912 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
913 70
                    $this->computeChangeSet($targetClass, $entry);
914 70
                    break;
915
916 362
                case self::STATE_MANAGED:
917 362
                    if ($targetClass->isEmbeddedDocument) {
918 179
                        [, $knownParent ] = $this->getParentAssociation($entry);
0 ignored issues
show
Bug introduced by
The variable $knownParent does not exist. Did you forget to declare it?

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

Loading history...
919 179
                        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 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
935 179
                        $this->computeChangeSet($targetClass, $entry);
936
                    }
937 362
                    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 366
                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 450
    }
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 641
    private function persistNew(ClassMetadata $class, object $document) : void
997
    {
998 641
        $this->lifecycleEventManager->prePersist($class, $document);
999 641
        $oid    = spl_object_hash($document);
1000 641
        $upsert = false;
1001 641
        if ($class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1002 641
            $idValue = $class->getIdentifierValue($document);
1003 641
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1004
1005 641
            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 640
            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 639
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1020 560
                $idValue = $class->idGenerator->generate($this->dm, $document);
1021 560
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1022 560
                $class->setIdentifierValue($document, $idValue);
1023
            }
1024
1025 639
            $this->documentIdentifiers[$oid] = $idValue;
1026
        } else {
1027
            // this is for embedded documents without identifiers
1028 152
            $this->documentIdentifiers[$oid] = $oid;
1029
        }
1030
1031 639
        $this->documentStates[$oid] = self::STATE_MANAGED;
1032
1033 639
        if ($upsert) {
1034 89
            $this->scheduleForUpsert($class, $document);
1035
        } else {
1036 569
            $this->scheduleForInsert($class, $document);
1037
        }
1038 639
    }
1039
1040
    /**
1041
     * Executes all document insertions for documents of the specified type.
1042
     */
1043 530
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1044
    {
1045 530
        $persister = $this->getDocumentPersister($class->name);
1046
1047 530
        foreach ($documents as $oid => $document) {
1048 530
            $persister->addInsert($document);
1049 530
            unset($this->documentInsertions[$oid]);
1050
        }
1051
1052 530
        $persister->executeInserts($options);
1053
1054 519
        foreach ($documents as $document) {
1055 519
            $this->lifecycleEventManager->postPersist($class, $document);
1056
        }
1057 519
    }
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 236
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1082
    {
1083 236
        if ($class->isReadOnly) {
1084
            return;
1085
        }
1086
1087 236
        $className = $class->name;
1088 236
        $persister = $this->getDocumentPersister($className);
1089
1090 236
        foreach ($documents as $oid => $document) {
1091 236
            $this->lifecycleEventManager->preUpdate($class, $document);
1092
1093 236
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1094 235
                $persister->update($document, $options);
1095
            }
1096
1097 229
            unset($this->documentUpdates[$oid]);
1098
1099 229
            $this->lifecycleEventManager->postUpdate($class, $document);
1100
        }
1101 228
    }
1102
1103
    /**
1104
     * Executes all document deletions for documents of the specified type.
1105
     */
1106 79
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1107
    {
1108 79
        $persister = $this->getDocumentPersister($class->name);
1109
1110 79
        foreach ($documents as $oid => $document) {
1111 79
            if (! $class->isEmbeddedDocument) {
1112 36
                $persister->delete($document, $options);
1113
            }
1114
            unset(
1115 77
                $this->documentDeletions[$oid],
1116 77
                $this->documentIdentifiers[$oid],
1117 77
                $this->originalDocumentData[$oid]
1118
            );
1119
1120
            // Clear snapshot information for any referenced PersistentCollection
1121
            // http://www.doctrine-project.org/jira/browse/MODM-95
1122 77
            foreach ($class->associationMappings as $fieldMapping) {
1123 53
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1124 38
                    continue;
1125
                }
1126
1127 33
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1128 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1129 7
                    continue;
1130
                }
1131
1132 29
                $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 77
            $this->documentStates[$oid] = self::STATE_NEW;
1138
1139 77
            $this->lifecycleEventManager->postRemove($class, $document);
1140
        }
1141 77
    }
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 572
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1151
    {
1152 572
        $oid = spl_object_hash($document);
1153
1154 572
        if (isset($this->documentUpdates[$oid])) {
1155
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1156
        }
1157 572
        if (isset($this->documentDeletions[$oid])) {
1158
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1159
        }
1160 572
        if (isset($this->documentInsertions[$oid])) {
1161
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1162
        }
1163
1164 572
        $this->documentInsertions[$oid] = $document;
1165
1166 572
        if (! isset($this->documentIdentifiers[$oid])) {
1167 3
            return;
1168
        }
1169
1170 569
        $this->addToIdentityMap($document);
1171 569
    }
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 110
    public function isScheduledForInsert(object $document) : bool
1205
    {
1206 110
        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 245
    public function scheduleForUpdate(object $document) : void
1223
    {
1224 245
        $oid = spl_object_hash($document);
1225 245
        if (! isset($this->documentIdentifiers[$oid])) {
1226
            throw new InvalidArgumentException('Document has no identity.');
1227
        }
1228
1229 245
        if (isset($this->documentDeletions[$oid])) {
1230
            throw new InvalidArgumentException('Document is removed.');
1231
        }
1232
1233 245
        if (isset($this->documentUpdates[$oid])
1234 245
            || isset($this->documentInsertions[$oid])
1235 245
            || isset($this->documentUpserts[$oid])) {
1236 104
            return;
1237
        }
1238
1239 243
        $this->documentUpdates[$oid] = $document;
1240 243
    }
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 84
    public function scheduleForDelete(object $document) : void
1263
    {
1264 84
        $oid = spl_object_hash($document);
1265
1266 84
        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 83
        if (! $this->isInIdentityMap($document)) {
1275 2
            return; // ignore
1276
        }
1277
1278 82
        $this->removeFromIdentityMap($document);
1279 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1280
1281 82
        if (isset($this->documentUpdates[$oid])) {
1282
            unset($this->documentUpdates[$oid]);
1283
        }
1284 82
        if (isset($this->documentDeletions[$oid])) {
1285
            return;
1286
        }
1287
1288 82
        $this->documentDeletions[$oid] = $document;
1289 82
    }
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 257
    public function isDocumentScheduled(object $document) : bool
1304
    {
1305 257
        $oid = spl_object_hash($document);
1306 257
        return isset($this->documentInsertions[$oid]) ||
1307 139
            isset($this->documentUpserts[$oid]) ||
1308 129
            isset($this->documentUpdates[$oid]) ||
1309 257
            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 680
    public function addToIdentityMap(object $document) : bool
1323
    {
1324 680
        $class = $this->dm->getClassMetadata(get_class($document));
1325 680
        $id    = $this->getIdForIdentityMap($document);
1326
1327 680
        if (isset($this->identityMap[$class->name][$id])) {
1328 44
            return false;
1329
        }
1330
1331 680
        $this->identityMap[$class->name][$id] = $document;
1332
1333 680
        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 680
            ( ! $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 680
        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 645
    public function getDocumentState(object $document, ?int $assume = null) : int
1350
    {
1351 645
        $oid = spl_object_hash($document);
1352
1353 645
        if (isset($this->documentStates[$oid])) {
1354 406
            return $this->documentStates[$oid];
1355
        }
1356
1357 644
        $class = $this->dm->getClassMetadata(get_class($document));
1358
1359 644
        if ($class->isEmbeddedDocument) {
1360 192
            return self::STATE_NEW;
1361
        }
1362
1363 641
        if ($assume !== null) {
1364 639
            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 96
    public function removeFromIdentityMap(object $document) : bool
1410
    {
1411 96
        $oid = spl_object_hash($document);
1412
1413
        // Check if id is registered first
1414 96
        if (! isset($this->documentIdentifiers[$oid])) {
1415
            return false;
1416
        }
1417
1418 96
        $class = $this->dm->getClassMetadata(get_class($document));
1419 96
        $id    = $this->getIdForIdentityMap($document);
1420
1421 96
        if (isset($this->identityMap[$class->name][$id])) {
1422 96
            unset($this->identityMap[$class->name][$id]);
1423 96
            $this->documentStates[$oid] = self::STATE_DETACHED;
1424 96
            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 92
    public function isInIdentityMap(object $document) : bool
1490
    {
1491 92
        $oid = spl_object_hash($document);
1492
1493 92
        if (! isset($this->documentIdentifiers[$oid])) {
1494 6
            return false;
1495
        }
1496
1497 90
        $class = $this->dm->getClassMetadata(get_class($document));
1498 90
        $id    = $this->getIdForIdentityMap($document);
1499
1500 90
        return isset($this->identityMap[$class->name][$id]);
1501
    }
1502
1503 680
    private function getIdForIdentityMap(object $document) : string
1504
    {
1505 680
        $class = $this->dm->getClassMetadata(get_class($document));
1506
1507 680
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

Loading history...
1758
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1759
                                } else {
1760
                                    $other = $this
1761
                                        ->dm
1762
                                        ->getProxyFactory()
1763
                                        ->getProxy($targetClass, $relatedId);
1764
                                    $this->registerManaged($other, $relatedId, []);
1765
                                }
1766
                            }
1767
1768 5
                            $prop->setValue($managedCopy, $other);
1769
                        }
1770
                    } else {
1771 10
                        $mergeCol = $prop->getValue($document);
1772
1773 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1774
                            /* Do not merge fields marked lazy that have not
1775
                             * been fetched. Keep the lazy persistent collection
1776
                             * of the managed copy.
1777
                             */
1778 3
                            continue;
1779
                        }
1780
1781 10
                        $managedCol = $prop->getValue($managedCopy);
1782
1783 10
                        if (! $managedCol) {
1784 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1785 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1786 1
                            $prop->setValue($managedCopy, $managedCol);
1787 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1788
                        }
1789
1790
                        /* Note: do not process association's target documents.
1791
                         * They will be handled during the cascade. Initialize
1792
                         * and, if necessary, clear $managedCol for now.
1793
                         */
1794 10
                        if ($assoc2['isCascadeMerge']) {
1795 10
                            $managedCol->initialize();
1796
1797
                            // If $managedCol differs from the merged collection, clear and set dirty
1798 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1799 3
                                $managedCol->unwrap()->clear();
1800 3
                                $managedCol->setDirty(true);
1801
1802 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1803
                                    $this->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 638
    private function cascadePersist(object $document, array &$visited) : void
2023
    {
2024 638
        $class = $this->dm->getClassMetadata(get_class($document));
2025
2026 638
        $associationMappings = array_filter(
2027 638
            $class->associationMappings,
2028
            static function ($assoc) {
2029 493
                return $assoc['isCascadePersist'];
2030 638
            }
2031
        );
2032
2033 638
        foreach ($associationMappings as $fieldName => $mapping) {
2034 441
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2035
2036 441
            if ($relatedDocuments instanceof Collection || is_array($relatedDocuments)) {
0 ignored issues
show
Bug introduced by
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

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

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
2661 110
            $this->documentIdentifiers[$oid] = $oid;
2662
        } else {
2663 375
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2664
        }
2665
2666 381
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2667 381
        $this->originalDocumentData[$oid] = $data;
2668 381
        $this->addToIdentityMap($document);
2669 381
    }
2670
2671
    /**
2672
     * INTERNAL:
2673
     * Clears the property changeset of the document with the given OID.
2674
     */
2675
    public function clearDocumentChangeSet(string $oid)
2676
    {
2677
        $this->documentChangeSets[$oid] = [];
2678
    }
2679
2680
    /* PropertyChangedListener implementation */
2681
2682
    /**
2683
     * Notifies this UnitOfWork of a property change in a document.
2684
     *
2685
     * @param object $document     The document that owns the property.
2686
     * @param string $propertyName The name of the property that changed.
2687
     * @param mixed  $oldValue     The old value of the property.
2688
     * @param mixed  $newValue     The new value of the property.
2689
     */
2690 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2691
    {
2692 2
        $oid   = spl_object_hash($document);
2693 2
        $class = $this->dm->getClassMetadata(get_class($document));
2694
2695 2
        if (! isset($class->fieldMappings[$propertyName])) {
2696 1
            return; // ignore non-persistent fields
2697
        }
2698
2699
        // Update changeset and mark document for synchronization
2700 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2701 2
        if (isset($this->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