Completed
Pull Request — master (#1984)
by Maciej
22:01
created

UnitOfWork::getOwningDocument()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

Changes 0
Metric Value
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
rs 9.7333
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.0123
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use function array_filter;
29
use function count;
30
use function get_class;
31
use function in_array;
32
use function is_array;
33
use function is_object;
34
use function method_exists;
35
use function preg_match;
36
use function serialize;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * The UnitOfWork is responsible for tracking changes to objects during an
42
 * "object-level" transaction and for writing out changes to the database
43
 * in the correct order.
44
 */
45
final class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    public const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    public const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    public const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    public const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = [];
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = [];
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     *
104
     * @var array
105
     */
106
    private $originalDocumentData = [];
107
108
    /**
109
     * Map of document changes. Keys are object ids (spl_object_hash).
110
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
111
     *
112
     * @var array
113
     */
114
    private $documentChangeSets = [];
115
116
    /**
117
     * The (cached) states of any known documents.
118
     * Keys are object ids (spl_object_hash).
119
     *
120
     * @var array
121
     */
122
    private $documentStates = [];
123
124
    /**
125
     * Map of documents that are scheduled for dirty checking at commit time.
126
     *
127
     * Documents are grouped by their class name, and then indexed by their SPL
128
     * object hash. This is only used for documents with a change tracking
129
     * policy of DEFERRED_EXPLICIT.
130
     *
131
     * @var array
132
     */
133
    private $scheduledForSynchronization = [];
134
135
    /**
136
     * A list of all pending document insertions.
137
     *
138
     * @var array
139
     */
140
    private $documentInsertions = [];
141
142
    /**
143
     * A list of all pending document updates.
144
     *
145
     * @var array
146
     */
147
    private $documentUpdates = [];
148
149
    /**
150
     * A list of all pending document upserts.
151
     *
152
     * @var array
153
     */
154
    private $documentUpserts = [];
155
156
    /**
157
     * A list of all pending document deletions.
158
     *
159
     * @var array
160
     */
161
    private $documentDeletions = [];
162
163
    /**
164
     * All pending collection deletions.
165
     *
166
     * @var array
167
     */
168
    private $collectionDeletions = [];
169
170
    /**
171
     * All pending collection updates.
172
     *
173
     * @var array
174
     */
175
    private $collectionUpdates = [];
176
177
    /**
178
     * A list of documents related to collections scheduled for update or deletion
179
     *
180
     * @var array
181
     */
182
    private $hasScheduledCollections = [];
183
184
    /**
185
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
186
     * At the end of the UnitOfWork all these collections will make new snapshots
187
     * of their data.
188
     *
189
     * @var array
190
     */
191
    private $visitedCollections = [];
192
193
    /**
194
     * The DocumentManager that "owns" this UnitOfWork instance.
195
     *
196
     * @var DocumentManager
197
     */
198
    private $dm;
199
200
    /**
201
     * The EventManager used for dispatching events.
202
     *
203
     * @var EventManager
204
     */
205
    private $evm;
206
207
    /**
208
     * Additional documents that are scheduled for removal.
209
     *
210
     * @var array
211
     */
212
    private $orphanRemovals = [];
213
214
    /**
215
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
216
     *
217
     * @var HydratorFactory
218
     */
219
    private $hydratorFactory;
220
221
    /**
222
     * The document persister instances used to persist document instances.
223
     *
224
     * @var array
225
     */
226
    private $persisters = [];
227
228
    /**
229
     * The collection persister instance used to persist changes to collections.
230
     *
231
     * @var Persisters\CollectionPersister
232
     */
233
    private $collectionPersister;
234
235
    /**
236
     * The persistence builder instance used in DocumentPersisters.
237
     *
238
     * @var PersistenceBuilder|null
239
     */
240
    private $persistenceBuilder;
241
242
    /**
243
     * Array of parent associations between embedded documents.
244
     *
245
     * @var array
246
     */
247
    private $parentAssociations = [];
248
249
    /** @var LifecycleEventManager */
250
    private $lifecycleEventManager;
251
252
    /**
253
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
254
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
255
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
256
     *
257
     * @var array
258
     */
259
    private $embeddedDocumentsRegistry = [];
260
261
    /** @var int */
262
    private $commitsInProgress = 0;
263
264
    /**
265
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
266
     */
267 1756
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
268
    {
269 1756
        $this->dm                    = $dm;
270 1756
        $this->evm                   = $evm;
271 1756
        $this->hydratorFactory       = $hydratorFactory;
272 1756
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
273 1756
    }
274
275
    /**
276
     * Factory for returning new PersistenceBuilder instances used for preparing data into
277
     * queries for insert persistence.
278
     *
279
     * @internal
280
     */
281 1199
    public function getPersistenceBuilder() : PersistenceBuilder
282
    {
283 1199
        if (! $this->persistenceBuilder) {
284 1199
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
285
        }
286 1199
        return $this->persistenceBuilder;
287
    }
288
289
    /**
290
     * Sets the parent association for a given embedded document.
291
     *
292
     * @internal
293
     */
294 209
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
295
    {
296 209
        $oid                                   = spl_object_hash($document);
297 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
298 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
299 209
    }
300
301
    /**
302
     * Gets the parent association for a given embedded document.
303
     *
304
     *     <code>
305
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
306
     *     </code>
307
     */
308 233
    public function getParentAssociation(object $document) : ?array
309
    {
310 233
        $oid = spl_object_hash($document);
311
312 233
        return $this->parentAssociations[$oid] ?? null;
313
    }
314
315
    /**
316
     * Get the document persister instance for the given document name
317
     */
318 1197
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
319
    {
320 1197
        if (! isset($this->persisters[$documentName])) {
321 1197
            $class                           = $this->dm->getClassMetadata($documentName);
322 1197
            $pb                              = $this->getPersistenceBuilder();
323 1197
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
324
        }
325 1197
        return $this->persisters[$documentName];
326
    }
327
328
    /**
329
     * Get the collection persister instance.
330
     */
331 1197
    public function getCollectionPersister() : CollectionPersister
332
    {
333 1197
        if (! isset($this->collectionPersister)) {
334 1197
            $pb                        = $this->getPersistenceBuilder();
335 1197
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
336
        }
337 1197
        return $this->collectionPersister;
338
    }
339
340
    /**
341
     * Set the document persister instance to use for the given document name
342
     *
343
     * @internal
344
     */
345
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
346
    {
347
        $this->persisters[$documentName] = $persister;
348
    }
349
350
    /**
351
     * Commits the UnitOfWork, executing all operations that have been postponed
352
     * up to this point. The state of all managed documents will be synchronized with
353
     * the database.
354
     *
355
     * The operations are executed in the following order:
356
     *
357
     * 1) All document insertions
358
     * 2) All document updates
359
     * 3) All document deletions
360
     *
361
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
362
     */
363 611
    public function commit(array $options = []) : void
364
    {
365
        // Raise preFlush
366 611
        if ($this->evm->hasListeners(Events::preFlush)) {
367
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
368
        }
369
370
        // Compute changes done since last commit.
371 611
        $this->computeChangeSets();
372
373 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...
374 256
            $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...
375 214
            $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...
376 197
            $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...
377 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...
378 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...
379 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...
380
        ) {
381 22
            return; // Nothing to do.
382
        }
383
384 607
        $this->commitsInProgress++;
385 607
        if ($this->commitsInProgress > 1) {
386
            throw MongoDBException::commitInProgress();
387
        }
388
        try {
389 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...
390 55
                foreach ($this->orphanRemovals as $removal) {
391 55
                    $this->remove($removal);
392
                }
393
            }
394
395
            // Raise onFlush
396 607
            if ($this->evm->hasListeners(Events::onFlush)) {
397 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
398
            }
399
400 606
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
401 85
                [$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...
402 85
                $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...
403
            }
404
405 606
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
406 531
                [$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 531
                $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...
408
            }
409
410 593
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
411 235
                [$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 235
                $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...
413
            }
414
415 593
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
416 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...
417 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...
418
            }
419
420
            // Raise postFlush
421 593
            if ($this->evm->hasListeners(Events::postFlush)) {
422
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
423
            }
424
425
            // Clear up
426 593
            $this->documentInsertions          =
427 593
            $this->documentUpserts             =
428 593
            $this->documentUpdates             =
429 593
            $this->documentDeletions           =
430 593
            $this->documentChangeSets          =
431 593
            $this->collectionUpdates           =
432 593
            $this->collectionDeletions         =
433 593
            $this->visitedCollections          =
434 593
            $this->scheduledForSynchronization =
435 593
            $this->orphanRemovals              =
436 593
            $this->hasScheduledCollections     = [];
437 593
        } finally {
438 607
            $this->commitsInProgress--;
439
        }
440 593
    }
441
442
    /**
443
     * Groups a list of scheduled documents by their class.
444
     */
445 606
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
446
    {
447 606
        if (empty($documents)) {
448 606
            return [];
449
        }
450 605
        $divided = [];
451 605
        $embeds  = [];
452 605
        foreach ($documents as $oid => $d) {
453 605
            $className = get_class($d);
454 605
            if (isset($embeds[$className])) {
455 81
                continue;
456
            }
457 605
            if (isset($divided[$className])) {
458 166
                $divided[$className][1][$oid] = $d;
459 166
                continue;
460
            }
461 605
            $class = $this->dm->getClassMetadata($className);
462 605
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
463 183
                $embeds[$className] = true;
464 183
                continue;
465
            }
466 605
            if (empty($divided[$class->name])) {
467 605
                $divided[$class->name] = [$class, [$oid => $d]];
468
            } else {
469 4
                $divided[$class->name][1][$oid] = $d;
470
            }
471
        }
472 605
        return $divided;
473
    }
474
475
    /**
476
     * Compute changesets of all documents scheduled for insertion.
477
     *
478
     * Embedded documents will not be processed.
479
     */
480 616
    private function computeScheduleInsertsChangeSets() : void
481
    {
482 616
        foreach ($this->documentInsertions as $document) {
483 544
            $class = $this->dm->getClassMetadata(get_class($document));
484 544
            if ($class->isEmbeddedDocument) {
485 165
                continue;
486
            }
487
488 538
            $this->computeChangeSet($class, $document);
489
        }
490 615
    }
491
492
    /**
493
     * Compute changesets of all documents scheduled for upsert.
494
     *
495
     * Embedded documents will not be processed.
496
     */
497 615
    private function computeScheduleUpsertsChangeSets() : void
498
    {
499 615
        foreach ($this->documentUpserts as $document) {
500 84
            $class = $this->dm->getClassMetadata(get_class($document));
501 84
            if ($class->isEmbeddedDocument) {
502
                continue;
503
            }
504
505 84
            $this->computeChangeSet($class, $document);
506
        }
507 615
    }
508
509
    /**
510
     * Gets the changeset for a document.
511
     *
512
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
513
     */
514 613
    public function getDocumentChangeSet(object $document) : array
515
    {
516 613
        $oid = spl_object_hash($document);
517
518 613
        return $this->documentChangeSets[$oid] ?? [];
519
    }
520
521
    /**
522
     * Sets the changeset for a document.
523
     *
524
     * @internal
525
     */
526 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
527
    {
528 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
529 1
    }
530
531
    /**
532
     * Get a documents actual data, flattening all the objects to arrays.
533
     *
534
     * @internal
535
     *
536
     * @return array
537
     */
538 616
    public function getDocumentActualData(object $document) : array
539
    {
540 616
        $class      = $this->dm->getClassMetadata(get_class($document));
541 616
        $actualData = [];
542 616
        foreach ($class->reflFields as $name => $refProp) {
543 616
            $mapping = $class->fieldMappings[$name];
544
            // skip not saved fields
545 616
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
546 199
                continue;
547
            }
548 616
            $value = $refProp->getValue($document);
549 616
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
550 616
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
551
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
552 401
                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...
553 147
                    $value = new ArrayCollection($value);
554
                }
555
556
                // Inject PersistentCollection
557 401
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
558 401
                $coll->setOwner($document, $mapping);
559 401
                $coll->setDirty(! $value->isEmpty());
560 401
                $class->reflFields[$name]->setValue($document, $coll);
561 401
                $actualData[$name] = $coll;
562
            } else {
563 616
                $actualData[$name] = $value;
564
            }
565
        }
566 616
        return $actualData;
567
    }
568
569
    /**
570
     * Computes the changes that happened to a single document.
571
     *
572
     * Modifies/populates the following properties:
573
     *
574
     * {@link originalDocumentData}
575
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
576
     * then it was not fetched from the database and therefore we have no original
577
     * document data yet. All of the current document data is stored as the original document data.
578
     *
579
     * {@link documentChangeSets}
580
     * The changes detected on all properties of the document are stored there.
581
     * A change is a tuple array where the first entry is the old value and the second
582
     * entry is the new value of the property. Changesets are used by persisters
583
     * to INSERT/UPDATE the persistent document state.
584
     *
585
     * {@link documentUpdates}
586
     * If the document is already fully MANAGED (has been fetched from the database before)
587
     * and any changes to its properties are detected, then a reference to the document is stored
588
     * there to mark it for an update.
589
     */
590 612
    public function computeChangeSet(ClassMetadata $class, object $document) : void
591
    {
592 612
        if (! $class->isInheritanceTypeNone()) {
593 185
            $class = $this->dm->getClassMetadata(get_class($document));
594
        }
595
596
        // Fire PreFlush lifecycle callbacks
597 612
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
598 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
599
        }
600
601 612
        $this->computeOrRecomputeChangeSet($class, $document);
602 611
    }
603
604
    /**
605
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
606
     */
607 612
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
608
    {
609 612
        $oid           = spl_object_hash($document);
610 612
        $actualData    = $this->getDocumentActualData($document);
611 612
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
612 612
        if ($isNewDocument) {
613
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
614
            // These result in an INSERT.
615 611
            $this->originalDocumentData[$oid] = $actualData;
616 611
            $changeSet                        = [];
617 611
            foreach ($actualData as $propName => $actualValue) {
618
                /* At this PersistentCollection shouldn't be here, probably it
619
                 * was cloned and its ownership must be fixed
620
                 */
621 611
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
622
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
623
                    $actualValue           = $actualData[$propName];
624
                }
625
                // ignore inverse side of reference relationship
626 611
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
627 189
                    continue;
628
                }
629 611
                $changeSet[$propName] = [null, $actualValue];
630
            }
631 611
            $this->documentChangeSets[$oid] = $changeSet;
632
        } else {
633 296
            if ($class->isReadOnly) {
634 2
                return;
635
            }
636
            // Document is "fully" MANAGED: it was already fully persisted before
637
            // and we have a copy of the original data
638 294
            $originalData           = $this->originalDocumentData[$oid];
639 294
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
640 294
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
641 1
                $changeSet = $this->documentChangeSets[$oid];
642
            } else {
643 294
                $changeSet = [];
644
            }
645
646 294
            $gridFSMetadataProperty = null;
647
648 294
            if ($class->isFile) {
649
                try {
650 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
651 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
652 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...
653
                }
654
            }
655
656 294
            foreach ($actualData as $propName => $actualValue) {
657
                // skip not saved fields
658 294
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
659 294
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
660 4
                    continue;
661
                }
662
663 293
                $orgValue = $originalData[$propName] ?? null;
664
665
                // skip if value has not changed
666 293
                if ($orgValue === $actualValue) {
667 291
                    if (! $actualValue instanceof PersistentCollectionInterface) {
668 291
                        continue;
669
                    }
670
671 205
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
672
                        // consider dirty collections as changed as well
673 182
                        continue;
674
                    }
675
                }
676
677
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
678 253
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
679 14
                    if ($orgValue !== null) {
680 8
                        $this->scheduleOrphanRemoval($orgValue);
681
                    }
682 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
683 14
                    continue;
684
                }
685
686
                // if owning side of reference-one relationship
687 246
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
688 12
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
689 1
                        $this->scheduleOrphanRemoval($orgValue);
690
                    }
691
692 12
                    $changeSet[$propName] = [$orgValue, $actualValue];
693 12
                    continue;
694
                }
695
696 239
                if ($isChangeTrackingNotify) {
697 2
                    continue;
698
                }
699
700
                // ignore inverse side of reference relationship
701 238
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
702 6
                    continue;
703
                }
704
705
                // Persistent collection was exchanged with the "originally"
706
                // created one. This can only mean it was cloned and replaced
707
                // on another document.
708 236
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
709 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
710
                }
711
712
                // if embed-many or reference-many relationship
713 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
714 125
                    $changeSet[$propName] = [$orgValue, $actualValue];
715
                    /* If original collection was exchanged with a non-empty value
716
                     * and $set will be issued, there is no need to $unset it first
717
                     */
718 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
719 31
                        continue;
720
                    }
721 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
722 18
                        $this->scheduleCollectionDeletion($orgValue);
723
                    }
724 104
                    continue;
725
                }
726
727
                // skip equivalent date values
728 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
729
                    /** @var DateType $dateType */
730 37
                    $dateType      = Type::getType('date');
731 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
732 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
733
734 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...
735 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...
736
737 37
                    if ($orgTimestamp === $actualTimestamp) {
738 30
                        continue;
739
                    }
740
                }
741
742
                // regular field
743 131
                $changeSet[$propName] = [$orgValue, $actualValue];
744
            }
745 294
            if ($changeSet) {
746 242
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
747 18
                    ? $changeSet + $this->documentChangeSets[$oid]
748 239
                    : $changeSet;
749
750 242
                $this->originalDocumentData[$oid] = $actualData;
751 242
                $this->scheduleForUpdate($document);
752
            }
753
        }
754
755
        // Look for changes in associations of the document
756 612
        $associationMappings = array_filter(
757 612
            $class->associationMappings,
758
            static function ($assoc) {
759 471
                return empty($assoc['notSaved']);
760 612
            }
761
        );
762
763 612
        foreach ($associationMappings as $mapping) {
764 471
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
765
766 471
            if ($value === null) {
767 324
                continue;
768
            }
769
770 453
            $this->computeAssociationChanges($document, $mapping, $value);
771
772 452
            if (isset($mapping['reference'])) {
773 338
                continue;
774
            }
775
776 356
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
777
778 356
            foreach ($values as $obj) {
779 188
                $oid2 = spl_object_hash($obj);
780
781 188
                if (isset($this->documentChangeSets[$oid2])) {
782 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
783
                        // instance of $value is the same as it was previously otherwise there would be
784
                        // change set already in place
785 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
786
                    }
787
788 186
                    if (! $isNewDocument) {
789 87
                        $this->scheduleForUpdate($document);
790
                    }
791
792 186
                    break;
793
                }
794
            }
795
        }
796 611
    }
797
798
    /**
799
     * Computes all the changes that have been done to documents and collections
800
     * since the last commit and stores these changes in the _documentChangeSet map
801
     * temporarily for access by the persisters, until the UoW commit is finished.
802
     */
803 616
    public function computeChangeSets() : void
804
    {
805 616
        $this->computeScheduleInsertsChangeSets();
806 615
        $this->computeScheduleUpsertsChangeSets();
807
808
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
809 615
        foreach ($this->identityMap as $className => $documents) {
810 615
            $class = $this->dm->getClassMetadata($className);
811 615
            if ($class->isEmbeddedDocument) {
812
                /* we do not want to compute changes to embedded documents up front
813
                 * in case embedded document was replaced and its changeset
814
                 * would corrupt data. Embedded documents' change set will
815
                 * be calculated by reachability from owning document.
816
                 */
817 178
                continue;
818
            }
819
820
            // If change tracking is explicit or happens through notification, then only compute
821
            // changes on document of that type that are explicitly marked for synchronization.
822
            switch (true) {
823 615
                case $class->isChangeTrackingDeferredImplicit():
824 614
                    $documentsToProcess = $documents;
825 614
                    break;
826
827 3
                case isset($this->scheduledForSynchronization[$className]):
828 2
                    $documentsToProcess = $this->scheduledForSynchronization[$className];
829 2
                    break;
830
831
                default:
832 3
                    $documentsToProcess = [];
833
            }
834
835 615
            foreach ($documentsToProcess as $document) {
836
                // Ignore uninitialized proxy objects
837 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...
838 9
                    continue;
839
                }
840
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
841 610
                $oid = spl_object_hash($document);
842 610
                if (isset($this->documentInsertions[$oid])
843 337
                    || isset($this->documentUpserts[$oid])
844 291
                    || isset($this->documentDeletions[$oid])
845 610
                    || ! isset($this->documentStates[$oid])
846
                ) {
847 608
                    continue;
848
                }
849
850 291
                $this->computeChangeSet($class, $document);
851
            }
852
        }
853 615
    }
854
855
    /**
856
     * Computes the changes of an association.
857
     *
858
     * @param mixed $value The value of the association.
859
     *
860
     * @throws InvalidArgumentException
861
     */
862 453
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
863
    {
864 453
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
865 453
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
866 453
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
867
868 453
        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...
869 7
            return;
870
        }
871
872 452
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
873 257
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
874 253
                $this->scheduleCollectionUpdate($value);
875
            }
876
877 257
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
878 257
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
879 257
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
880 151
                $value->initialize();
881 151
                foreach ($value->getDeletedDocuments() as $orphan) {
882 25
                    $this->scheduleOrphanRemoval($orphan);
883
                }
884
            }
885
        }
886
887
        // Look through the documents, and in any of their associations,
888
        // for transient (new) documents, recursively. ("Persistence by reachability")
889
        // Unwrap. Uninitialized collections will simply be empty.
890 452
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
891
892 452
        $count = 0;
893 452
        foreach ($unwrappedValue as $key => $entry) {
894 366
            if (! is_object($entry)) {
895 1
                throw new InvalidArgumentException(
896 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
897
                );
898
            }
899
900 365
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
901
902 365
            $state = $this->getDocumentState($entry, self::STATE_NEW);
903
904
            // Handle "set" strategy for multi-level hierarchy
905 365
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
906 365
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
907
908 365
            $count++;
909
910
            switch ($state) {
911 365
                case self::STATE_NEW:
912 70
                    if (! $assoc['isCascadePersist']) {
913
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
914
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
915
                            . ' Explicitly persist the new document or configure cascading persist operations'
916
                            . ' on the relationship.');
917
                    }
918
919 70
                    $this->persistNew($targetClass, $entry);
920 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
921 70
                    $this->computeChangeSet($targetClass, $entry);
922 70
                    break;
923
924 361
                case self::STATE_MANAGED:
925 361
                    if ($targetClass->isEmbeddedDocument) {
926 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...
927 179
                        if ($knownParent && $knownParent !== $parentDocument) {
928 6
                            $entry = clone $entry;
929 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
930 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
931 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
932 3
                                $poid = spl_object_hash($parentDocument);
933 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
934 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
935
                                }
936
                            } else {
937
                                // must use unwrapped value to not trigger orphan removal
938 4
                                $unwrappedValue[$key] = $entry;
939
                            }
940 6
                            $this->persistNew($targetClass, $entry);
941
                        }
942 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
943 179
                        $this->computeChangeSet($targetClass, $entry);
944
                    }
945 361
                    break;
946
947 1
                case self::STATE_REMOVED:
948
                    // Consume the $value as array (it's either an array or an ArrayAccess)
949
                    // and remove the element from Collection.
950 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
951
                        unset($value[$key]);
952
                    }
953 1
                    break;
954
955
                case self::STATE_DETACHED:
956
                    // Can actually not happen right now as we assume STATE_NEW,
957
                    // so the exception will be raised from the DBAL layer (constraint violation).
958
                    throw new InvalidArgumentException('A detached document was found through a '
959
                        . 'relationship during cascading a persist operation.');
960
961
                default:
962
                    // MANAGED associated documents are already taken into account
963
                    // during changeset calculation anyway, since they are in the identity map.
964
            }
965
        }
966 451
    }
967
968
    /**
969
     * Computes the changeset of an individual document, independently of the
970
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
971
     *
972
     * The passed document must be a managed document. If the document already has a change set
973
     * because this method is invoked during a commit cycle then the change sets are added.
974
     * whereby changes detected in this method prevail.
975
     *
976
     * @throws InvalidArgumentException If the passed document is not MANAGED.
977
     */
978 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
979
    {
980
        // Ignore uninitialized proxy objects
981 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...
982 1
            return;
983
        }
984
985 18
        $oid = spl_object_hash($document);
986
987 18
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
988
            throw new InvalidArgumentException('Document must be managed.');
989
        }
990
991 18
        if (! $class->isInheritanceTypeNone()) {
992 2
            $class = $this->dm->getClassMetadata(get_class($document));
993
        }
994
995 18
        $this->computeOrRecomputeChangeSet($class, $document, true);
996 18
    }
997
998
    /**
999
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1000
     */
1001 644
    private function persistNew(ClassMetadata $class, object $document) : void
1002
    {
1003 644
        $this->lifecycleEventManager->prePersist($class, $document);
1004 644
        $oid    = spl_object_hash($document);
1005 644
        $upsert = false;
1006 644
        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...
1007 644
            $idValue = $class->getIdentifierValue($document);
1008 644
            $upsert  = ! $class->isEmbeddedDocument && $idValue !== null;
1009
1010 644
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1011 3
                throw new InvalidArgumentException(sprintf(
1012 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1013 3
                    get_class($document)
1014
                ));
1015
            }
1016
1017 643
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1018 1
                throw new InvalidArgumentException(sprintf(
1019 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1020 1
                    get_class($document)
1021
                ));
1022
            }
1023
1024 642
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null && $class->idGenerator !== null) {
1025 561
                $idValue = $class->idGenerator->generate($this->dm, $document);
1026 561
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1027 561
                $class->setIdentifierValue($document, $idValue);
1028
            }
1029
1030 642
            $this->documentIdentifiers[$oid] = $idValue;
1031
        } else {
1032
            // this is for embedded documents without identifiers
1033 152
            $this->documentIdentifiers[$oid] = $oid;
1034
        }
1035
1036 642
        $this->documentStates[$oid] = self::STATE_MANAGED;
1037
1038 642
        if ($upsert) {
1039 91
            $this->scheduleForUpsert($class, $document);
1040
        } else {
1041 570
            $this->scheduleForInsert($class, $document);
1042
        }
1043 642
    }
1044
1045
    /**
1046
     * Executes all document insertions for documents of the specified type.
1047
     */
1048 531
    private function executeInserts(ClassMetadata $class, array $documents, array $options = []) : void
1049
    {
1050 531
        $persister = $this->getDocumentPersister($class->name);
1051
1052 531
        foreach ($documents as $oid => $document) {
1053 531
            $persister->addInsert($document);
1054 531
            unset($this->documentInsertions[$oid]);
1055
        }
1056
1057 531
        $persister->executeInserts($options);
1058
1059 520
        foreach ($documents as $document) {
1060 520
            $this->lifecycleEventManager->postPersist($class, $document);
1061
        }
1062 520
    }
1063
1064
    /**
1065
     * Executes all document upserts for documents of the specified type.
1066
     */
1067 85
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = []) : void
1068
    {
1069 85
        $persister = $this->getDocumentPersister($class->name);
1070
1071 85
        foreach ($documents as $oid => $document) {
1072 85
            $persister->addUpsert($document);
1073 85
            unset($this->documentUpserts[$oid]);
1074
        }
1075
1076 85
        $persister->executeUpserts($options);
1077
1078 85
        foreach ($documents as $document) {
1079 85
            $this->lifecycleEventManager->postPersist($class, $document);
1080
        }
1081 85
    }
1082
1083
    /**
1084
     * Executes all document updates for documents of the specified type.
1085
     */
1086 235
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = []) : void
1087
    {
1088 235
        if ($class->isReadOnly) {
1089
            return;
1090
        }
1091
1092 235
        $className = $class->name;
1093 235
        $persister = $this->getDocumentPersister($className);
1094
1095 235
        foreach ($documents as $oid => $document) {
1096 235
            $this->lifecycleEventManager->preUpdate($class, $document);
1097
1098 235
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1099 234
                $persister->update($document, $options);
1100
            }
1101
1102 228
            unset($this->documentUpdates[$oid]);
1103
1104 228
            $this->lifecycleEventManager->postUpdate($class, $document);
1105
        }
1106 227
    }
1107
1108
    /**
1109
     * Executes all document deletions for documents of the specified type.
1110
     */
1111 79
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = []) : void
1112
    {
1113 79
        $persister = $this->getDocumentPersister($class->name);
1114
1115 79
        foreach ($documents as $oid => $document) {
1116 79
            if (! $class->isEmbeddedDocument) {
1117 36
                $persister->delete($document, $options);
1118
            }
1119
            unset(
1120 77
                $this->documentDeletions[$oid],
1121 77
                $this->documentIdentifiers[$oid],
1122 77
                $this->originalDocumentData[$oid]
1123
            );
1124
1125
            // Clear snapshot information for any referenced PersistentCollection
1126
            // http://www.doctrine-project.org/jira/browse/MODM-95
1127 77
            foreach ($class->associationMappings as $fieldMapping) {
1128 53
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1129 38
                    continue;
1130
                }
1131
1132 33
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1133 33
                if (! ($value instanceof PersistentCollectionInterface)) {
1134 7
                    continue;
1135
                }
1136
1137 29
                $value->clearSnapshot();
1138
            }
1139
1140
            // Document with this $oid after deletion treated as NEW, even if the $oid
1141
            // is obtained by a new document because the old one went out of scope.
1142 77
            $this->documentStates[$oid] = self::STATE_NEW;
1143
1144 77
            $this->lifecycleEventManager->postRemove($class, $document);
1145
        }
1146 77
    }
1147
1148
    /**
1149
     * Schedules a document for insertion into the database.
1150
     * If the document already has an identifier, it will be added to the
1151
     * identity map.
1152
     *
1153
     * @internal
1154
     *
1155
     * @throws InvalidArgumentException
1156
     */
1157 573
    public function scheduleForInsert(ClassMetadata $class, object $document) : void
1158
    {
1159 573
        $oid = spl_object_hash($document);
1160
1161 573
        if (isset($this->documentUpdates[$oid])) {
1162
            throw new InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1163
        }
1164 573
        if (isset($this->documentDeletions[$oid])) {
1165
            throw new InvalidArgumentException('Removed document can not be scheduled for insertion.');
1166
        }
1167 573
        if (isset($this->documentInsertions[$oid])) {
1168
            throw new InvalidArgumentException('Document can not be scheduled for insertion twice.');
1169
        }
1170
1171 573
        $this->documentInsertions[$oid] = $document;
1172
1173 573
        if (! isset($this->documentIdentifiers[$oid])) {
1174 3
            return;
1175
        }
1176
1177 570
        $this->addToIdentityMap($document);
1178 570
    }
1179
1180
    /**
1181
     * Schedules a document for upsert into the database and adds it to the
1182
     * identity map
1183
     *
1184
     * @internal
1185
     *
1186
     * @throws InvalidArgumentException
1187
     */
1188 94
    public function scheduleForUpsert(ClassMetadata $class, object $document) : void
1189
    {
1190 94
        $oid = spl_object_hash($document);
1191
1192 94
        if ($class->isEmbeddedDocument) {
1193
            throw new InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1194
        }
1195 94
        if (isset($this->documentUpdates[$oid])) {
1196
            throw new InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1197
        }
1198 94
        if (isset($this->documentDeletions[$oid])) {
1199
            throw new InvalidArgumentException('Removed document can not be scheduled for upsert.');
1200
        }
1201 94
        if (isset($this->documentUpserts[$oid])) {
1202
            throw new InvalidArgumentException('Document can not be scheduled for upsert twice.');
1203
        }
1204
1205 94
        $this->documentUpserts[$oid]     = $document;
1206 94
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1207 94
        $this->addToIdentityMap($document);
1208 94
    }
1209
1210
    /**
1211
     * Checks whether a document is scheduled for insertion.
1212
     */
1213 109
    public function isScheduledForInsert(object $document) : bool
1214
    {
1215 109
        return isset($this->documentInsertions[spl_object_hash($document)]);
1216
    }
1217
1218
    /**
1219
     * Checks whether a document is scheduled for upsert.
1220
     */
1221 5
    public function isScheduledForUpsert(object $document) : bool
1222
    {
1223 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1224
    }
1225
1226
    /**
1227
     * Schedules a document for being updated.
1228
     *
1229
     * @internal
1230
     *
1231
     * @throws InvalidArgumentException
1232
     */
1233 244
    public function scheduleForUpdate(object $document) : void
1234
    {
1235 244
        $oid = spl_object_hash($document);
1236 244
        if (! isset($this->documentIdentifiers[$oid])) {
1237
            throw new InvalidArgumentException('Document has no identity.');
1238
        }
1239
1240 244
        if (isset($this->documentDeletions[$oid])) {
1241
            throw new InvalidArgumentException('Document is removed.');
1242
        }
1243
1244 244
        if (isset($this->documentUpdates[$oid])
1245 244
            || isset($this->documentInsertions[$oid])
1246 244
            || isset($this->documentUpserts[$oid])) {
1247 104
            return;
1248
        }
1249
1250 242
        $this->documentUpdates[$oid] = $document;
1251 242
    }
1252
1253
    /**
1254
     * Checks whether a document is registered as dirty in the unit of work.
1255
     * Note: Is not very useful currently as dirty documents are only registered
1256
     * at commit time.
1257
     */
1258 21
    public function isScheduledForUpdate(object $document) : bool
1259
    {
1260 21
        return isset($this->documentUpdates[spl_object_hash($document)]);
1261
    }
1262
1263
    /**
1264
     * Checks whether a document is registered to be checked in the unit of work.
1265
     */
1266
    public function isScheduledForSynchronization(object $document) : bool
1267
    {
1268
        $class = $this->dm->getClassMetadata(get_class($document));
1269
        return isset($this->scheduledForSynchronization[$class->name][spl_object_hash($document)]);
1270
    }
1271
1272
    /**
1273
     * Schedules a document for deletion.
1274
     *
1275
     * @internal
1276
     */
1277 84
    public function scheduleForDelete(object $document) : void
1278
    {
1279 84
        $oid = spl_object_hash($document);
1280
1281 84
        if (isset($this->documentInsertions[$oid])) {
1282 2
            if ($this->isInIdentityMap($document)) {
1283 2
                $this->removeFromIdentityMap($document);
1284
            }
1285 2
            unset($this->documentInsertions[$oid]);
1286 2
            return; // document has not been persisted yet, so nothing more to do.
1287
        }
1288
1289 83
        if (! $this->isInIdentityMap($document)) {
1290 2
            return; // ignore
1291
        }
1292
1293 82
        $this->removeFromIdentityMap($document);
1294 82
        $this->documentStates[$oid] = self::STATE_REMOVED;
1295
1296 82
        if (isset($this->documentUpdates[$oid])) {
1297
            unset($this->documentUpdates[$oid]);
1298
        }
1299 82
        if (isset($this->documentDeletions[$oid])) {
1300
            return;
1301
        }
1302
1303 82
        $this->documentDeletions[$oid] = $document;
1304 82
    }
1305
1306
    /**
1307
     * Checks whether a document is registered as removed/deleted with the unit
1308
     * of work.
1309
     */
1310 5
    public function isScheduledForDelete(object $document) : bool
1311
    {
1312 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1313
    }
1314
1315
    /**
1316
     * Checks whether a document is scheduled for insertion, update or deletion.
1317
     *
1318
     * @internal
1319
     */
1320 256
    public function isDocumentScheduled(object $document) : bool
1321
    {
1322 256
        $oid = spl_object_hash($document);
1323 256
        return isset($this->documentInsertions[$oid]) ||
1324 138
            isset($this->documentUpserts[$oid]) ||
1325 128
            isset($this->documentUpdates[$oid]) ||
1326 256
            isset($this->documentDeletions[$oid]);
1327
    }
1328
1329
    /**
1330
     * Registers a document in the identity map.
1331
     *
1332
     * Note that documents in a hierarchy are registered with the class name of
1333
     * the root document. Identifiers are serialized before being used as array
1334
     * keys to allow differentiation of equal, but not identical, values.
1335
     *
1336
     * @internal
1337
     */
1338 682
    public function addToIdentityMap(object $document) : bool
1339
    {
1340 682
        $class = $this->dm->getClassMetadata(get_class($document));
1341 682
        $id    = $this->getIdForIdentityMap($document);
1342
1343 682
        if (isset($this->identityMap[$class->name][$id])) {
1344 44
            return false;
1345
        }
1346
1347 682
        $this->identityMap[$class->name][$id] = $document;
1348
1349 682
        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...
1350 682
            ( ! $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...
1351 2
            $document->addPropertyChangedListener($this);
1352
        }
1353
1354 682
        return true;
1355
    }
1356
1357
    /**
1358
     * Gets the state of a document with regard to the current unit of work.
1359
     *
1360
     * @param int|null $assume The state to assume if the state is not yet known (not MANAGED or REMOVED).
1361
     *                         This parameter can be set to improve performance of document state detection
1362
     *                         by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1363
     *                         is either known or does not matter for the caller of the method.
1364
     */
1365 647
    public function getDocumentState(object $document, ?int $assume = null) : int
1366
    {
1367 647
        $oid = spl_object_hash($document);
1368
1369 647
        if (isset($this->documentStates[$oid])) {
1370 404
            return $this->documentStates[$oid];
1371
        }
1372
1373 646
        $class = $this->dm->getClassMetadata(get_class($document));
1374
1375 646
        if ($class->isEmbeddedDocument) {
1376 192
            return self::STATE_NEW;
1377
        }
1378
1379 643
        if ($assume !== null) {
1380 642
            return $assume;
1381
        }
1382
1383
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1384
         * known. Note that you cannot remember the NEW or DETACHED state in
1385
         * _documentStates since the UoW does not hold references to such
1386
         * objects and the object hash can be reused. More generally, because
1387
         * the state may "change" between NEW/DETACHED without the UoW being
1388
         * aware of it.
1389
         */
1390 2
        $id = $class->getIdentifierObject($document);
1391
1392 2
        if ($id === null) {
1393 2
            return self::STATE_NEW;
1394
        }
1395
1396
        // Check for a version field, if available, to avoid a DB lookup.
1397 1
        if ($class->isVersioned && $class->versionField !== null) {
1398
            return $class->getFieldValue($document, $class->versionField)
1399
                ? self::STATE_DETACHED
1400
                : self::STATE_NEW;
1401
        }
1402
1403
        // Last try before DB lookup: check the identity map.
1404 1
        if ($this->tryGetById($id, $class)) {
1405
            return self::STATE_DETACHED;
1406
        }
1407
1408
        // DB lookup
1409 1
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1410 1
            return self::STATE_DETACHED;
1411
        }
1412
1413
        return self::STATE_NEW;
1414
    }
1415
1416
    /**
1417
     * Removes a document from the identity map. This effectively detaches the
1418
     * document from the persistence management of Doctrine.
1419
     *
1420
     * @internal
1421
     *
1422
     * @throws InvalidArgumentException
1423
     */
1424 96
    public function removeFromIdentityMap(object $document) : bool
1425
    {
1426 96
        $oid = spl_object_hash($document);
1427
1428
        // Check if id is registered first
1429 96
        if (! isset($this->documentIdentifiers[$oid])) {
1430
            return false;
1431
        }
1432
1433 96
        $class = $this->dm->getClassMetadata(get_class($document));
1434 96
        $id    = $this->getIdForIdentityMap($document);
1435
1436 96
        if (isset($this->identityMap[$class->name][$id])) {
1437 96
            unset($this->identityMap[$class->name][$id]);
1438 96
            $this->documentStates[$oid] = self::STATE_DETACHED;
1439 96
            return true;
1440
        }
1441
1442
        return false;
1443
    }
1444
1445
    /**
1446
     * Gets a document in the identity map by its identifier hash.
1447
     *
1448
     * @internal
1449
     *
1450
     * @param mixed $id Document identifier
1451
     *
1452
     * @throws InvalidArgumentException If the class does not have an identifier.
1453
     */
1454 37
    public function getById($id, ClassMetadata $class) : object
1455
    {
1456 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...
1457
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1458
        }
1459
1460 37
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1461
1462 37
        return $this->identityMap[$class->name][$serializedId];
1463
    }
1464
1465
    /**
1466
     * Tries to get a document by its identifier hash. If no document is found
1467
     * for the given hash, FALSE is returned.
1468
     *
1469
     * @internal
1470
     *
1471
     * @param mixed $id Document identifier
1472
     *
1473
     * @return mixed The found document or FALSE.
1474
     *
1475
     * @throws InvalidArgumentException If the class does not have an identifier.
1476
     */
1477 307
    public function tryGetById($id, ClassMetadata $class)
1478
    {
1479 307
        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...
1480
            throw new InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1481
        }
1482
1483 307
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1484
1485 307
        return $this->identityMap[$class->name][$serializedId] ?? false;
1486
    }
1487
1488
    /**
1489
     * Schedules a document for dirty-checking at commit-time.
1490
     *
1491
     * @internal
1492
     */
1493 2
    public function scheduleForSynchronization(object $document) : void
1494
    {
1495 2
        $class                                                                       = $this->dm->getClassMetadata(get_class($document));
1496 2
        $this->scheduledForSynchronization[$class->name][spl_object_hash($document)] = $document;
1497 2
    }
1498
1499
    /**
1500
     * Checks whether a document is registered in the identity map.
1501
     *
1502
     * @internal
1503
     */
1504 90
    public function isInIdentityMap(object $document) : bool
1505
    {
1506 90
        $oid = spl_object_hash($document);
1507
1508 90
        if (! isset($this->documentIdentifiers[$oid])) {
1509 6
            return false;
1510
        }
1511
1512 88
        $class = $this->dm->getClassMetadata(get_class($document));
1513 88
        $id    = $this->getIdForIdentityMap($document);
1514
1515 88
        return isset($this->identityMap[$class->name][$id]);
1516
    }
1517
1518 682
    private function getIdForIdentityMap(object $document) : string
1519
    {
1520 682
        $class = $this->dm->getClassMetadata(get_class($document));
1521
1522 682
        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...
1523 158
            $id = spl_object_hash($document);
1524
        } else {
1525 681
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1526 681
            $id = serialize($class->getDatabaseIdentifierValue($id));
1527
        }
1528
1529 682
        return $id;
1530
    }
1531
1532
    /**
1533
     * Checks whether an identifier exists in the identity map.
1534
     *
1535
     * @internal
1536
     */
1537
    public function containsId($id, string $rootClassName) : bool
1538
    {
1539
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1540
    }
1541
1542
    /**
1543
     * Persists a document as part of the current unit of work.
1544
     *
1545
     * @internal
1546
     *
1547
     * @throws MongoDBException If trying to persist MappedSuperclass.
1548
     * @throws InvalidArgumentException If there is something wrong with document's identifier.
1549
     */
1550 645
    public function persist(object $document) : void
1551
    {
1552 645
        $class = $this->dm->getClassMetadata(get_class($document));
1553 645
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1554 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1555
        }
1556 644
        $visited = [];
1557 644
        $this->doPersist($document, $visited);
1558 639
    }
1559
1560
    /**
1561
     * Saves a document as part of the current unit of work.
1562
     * This method is internally called during save() cascades as it tracks
1563
     * the already visited documents to prevent infinite recursions.
1564
     *
1565
     * NOTE: This method always considers documents that are not yet known to
1566
     * this UnitOfWork as NEW.
1567
     *
1568
     * @throws InvalidArgumentException
1569
     * @throws MongoDBException
1570
     */
1571 644
    private function doPersist(object $document, array &$visited) : void
1572
    {
1573 644
        $oid = spl_object_hash($document);
1574 644
        if (isset($visited[$oid])) {
1575 25
            return; // Prevent infinite recursion
1576
        }
1577
1578 644
        $visited[$oid] = $document; // Mark visited
1579
1580 644
        $class = $this->dm->getClassMetadata(get_class($document));
1581
1582 644
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1583
        switch ($documentState) {
1584 644
            case self::STATE_MANAGED:
1585
                // Nothing to do, except if policy is "deferred explicit"
1586 57
                if ($class->isChangeTrackingDeferredExplicit()) {
1587
                    $this->scheduleForSynchronization($document);
1588
                }
1589 57
                break;
1590 644
            case self::STATE_NEW:
1591 644
                if ($class->isFile) {
1592 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1593
                }
1594
1595 643
                $this->persistNew($class, $document);
1596 641
                break;
1597
1598 2
            case self::STATE_REMOVED:
1599
                // Document becomes managed again
1600 2
                unset($this->documentDeletions[$oid]);
1601
1602 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1603 2
                break;
1604
1605
            case self::STATE_DETACHED:
1606
                throw new InvalidArgumentException(
1607
                    'Behavior of persist() for a detached document is not yet defined.'
1608
                );
1609
1610
            default:
1611
                throw MongoDBException::invalidDocumentState($documentState);
1612
        }
1613
1614 641
        $this->cascadePersist($document, $visited);
1615 639
    }
1616
1617
    /**
1618
     * Deletes a document as part of the current unit of work.
1619
     *
1620
     * @internal
1621
     */
1622 83
    public function remove(object $document)
1623
    {
1624 83
        $visited = [];
1625 83
        $this->doRemove($document, $visited);
1626 83
    }
1627
1628
    /**
1629
     * Deletes a document as part of the current unit of work.
1630
     *
1631
     * This method is internally called during delete() cascades as it tracks
1632
     * the already visited documents to prevent infinite recursions.
1633
     *
1634
     * @throws MongoDBException
1635
     */
1636 83
    private function doRemove(object $document, array &$visited) : void
1637
    {
1638 83
        $oid = spl_object_hash($document);
1639 83
        if (isset($visited[$oid])) {
1640 1
            return; // Prevent infinite recursion
1641
        }
1642
1643 83
        $visited[$oid] = $document; // mark visited
1644
1645
        /* Cascade first, because scheduleForDelete() removes the entity from
1646
         * the identity map, which can cause problems when a lazy Proxy has to
1647
         * be initialized for the cascade operation.
1648
         */
1649 83
        $this->cascadeRemove($document, $visited);
1650
1651 83
        $class         = $this->dm->getClassMetadata(get_class($document));
1652 83
        $documentState = $this->getDocumentState($document);
1653
        switch ($documentState) {
1654 83
            case self::STATE_NEW:
1655 83
            case self::STATE_REMOVED:
1656
                // nothing to do
1657 1
                break;
1658 83
            case self::STATE_MANAGED:
1659 83
                $this->lifecycleEventManager->preRemove($class, $document);
1660 83
                $this->scheduleForDelete($document);
1661 83
                break;
1662
            case self::STATE_DETACHED:
1663
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1664
            default:
1665
                throw MongoDBException::invalidDocumentState($documentState);
1666
        }
1667 83
    }
1668
1669
    /**
1670
     * Merges the state of the given detached document into this UnitOfWork.
1671
     *
1672
     * @internal
1673
     */
1674 11
    public function merge(object $document) : object
1675
    {
1676 11
        $visited = [];
1677
1678 11
        return $this->doMerge($document, $visited);
1679
    }
1680
1681
    /**
1682
     * Executes a merge operation on a document.
1683
     *
1684
     * @throws InvalidArgumentException If the entity instance is NEW.
1685
     * @throws LockException If the document uses optimistic locking through a
1686
     *                       version attribute and the version check against the
1687
     *                       managed copy fails.
1688
     */
1689 11
    private function doMerge(object $document, array &$visited, ?object $prevManagedCopy = null, ?array $assoc = null) : object
1690
    {
1691 11
        $oid = spl_object_hash($document);
1692
1693 11
        if (isset($visited[$oid])) {
1694 1
            return $visited[$oid]; // Prevent infinite recursion
1695
        }
1696
1697 11
        $visited[$oid] = $document; // mark visited
1698
1699 11
        $class = $this->dm->getClassMetadata(get_class($document));
1700
1701
        /* First we assume DETACHED, although it can still be NEW but we can
1702
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1703
         * an identity, we need to fetch it from the DB anyway in order to
1704
         * merge. MANAGED documents are ignored by the merge operation.
1705
         */
1706 11
        $managedCopy = $document;
1707
1708 11
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1709 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...
1710
                $document->initializeProxy();
1711
            }
1712
1713 11
            $identifier = $class->getIdentifier();
1714
            // We always have one element in the identifier array but it might be null
1715 11
            $id          = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1716 11
            $managedCopy = null;
1717
1718
            // Try to fetch document from the database
1719 11
            if (! $class->isEmbeddedDocument && $id !== null) {
1720 11
                $managedCopy = $this->dm->find($class->name, $id);
1721
1722
                // Managed copy may be removed in which case we can't merge
1723 11
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1724
                    throw new InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1725
                }
1726
1727 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...
1728
                    $managedCopy->initializeProxy();
1729
                }
1730
            }
1731
1732 11
            if ($managedCopy === null) {
1733
                // Create a new managed instance
1734 4
                $managedCopy = $class->newInstance();
1735 4
                if ($id !== null) {
1736 3
                    $class->setIdentifierValue($managedCopy, $id);
1737
                }
1738 4
                $this->persistNew($class, $managedCopy);
1739
            }
1740
1741 11
            if ($class->isVersioned) {
1742
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1743
                $documentVersion    = $class->reflFields[$class->versionField]->getValue($document);
1744
1745
                // Throw exception if versions don't match
1746
                if ($managedCopyVersion !== $documentVersion) {
1747
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1748
                }
1749
            }
1750
1751
            // Merge state of $document into existing (managed) document
1752 11
            foreach ($class->reflClass->getProperties() as $prop) {
1753 11
                $name = $prop->name;
1754 11
                $prop->setAccessible(true);
1755 11
                if (! isset($class->associationMappings[$name])) {
1756 11
                    if (! $class->isIdentifier($name)) {
1757 11
                        $prop->setValue($managedCopy, $prop->getValue($document));
1758
                    }
1759
                } else {
1760 11
                    $assoc2 = $class->associationMappings[$name];
1761
1762 11
                    if ($assoc2['type'] === 'one') {
1763 5
                        $other = $prop->getValue($document);
1764
1765 5
                        if ($other === null) {
1766 2
                            $prop->setValue($managedCopy, null);
1767 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...
1768
                            // Do not merge fields marked lazy that have not been fetched
1769
                            continue;
1770 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1771
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1772
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1773
                                /** @var ClassMetadata $targetClass */
1774
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1775
                                $relatedId   = $targetClass->getIdentifierObject($other);
1776
1777
                                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...
1778
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1779
                                } else {
1780
                                    $other = $this
1781
                                        ->dm
1782
                                        ->getProxyFactory()
1783
                                        ->getProxy($targetClass, $relatedId);
1784
                                    $this->registerManaged($other, $relatedId, []);
1785
                                }
1786
                            }
1787
1788 5
                            $prop->setValue($managedCopy, $other);
1789
                        }
1790
                    } else {
1791 10
                        $mergeCol = $prop->getValue($document);
1792
1793 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1794
                            /* Do not merge fields marked lazy that have not
1795
                             * been fetched. Keep the lazy persistent collection
1796
                             * of the managed copy.
1797
                             */
1798 3
                            continue;
1799
                        }
1800
1801 10
                        $managedCol = $prop->getValue($managedCopy);
1802
1803 10
                        if (! $managedCol) {
1804 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1805 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1806 1
                            $prop->setValue($managedCopy, $managedCol);
1807 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1808
                        }
1809
1810
                        /* Note: do not process association's target documents.
1811
                         * They will be handled during the cascade. Initialize
1812
                         * and, if necessary, clear $managedCol for now.
1813
                         */
1814 10
                        if ($assoc2['isCascadeMerge']) {
1815 10
                            $managedCol->initialize();
1816
1817
                            // If $managedCol differs from the merged collection, clear and set dirty
1818 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1819 3
                                $managedCol->unwrap()->clear();
1820 3
                                $managedCol->setDirty(true);
1821
1822 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1823
                                    $this->scheduleForSynchronization($managedCopy);
1824
                                }
1825
                            }
1826
                        }
1827
                    }
1828
                }
1829
1830 11
                if (! $class->isChangeTrackingNotify()) {
1831 11
                    continue;
1832
                }
1833
1834
                // Just treat all properties as changed, there is no other choice.
1835
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1836
            }
1837
1838 11
            if ($class->isChangeTrackingDeferredExplicit()) {
1839
                $this->scheduleForSynchronization($document);
1840
            }
1841
        }
1842
1843 11
        if ($prevManagedCopy !== null) {
1844 5
            $assocField = $assoc['fieldName'];
1845 5
            $prevClass  = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1846
1847 5
            if ($assoc['type'] === 'one') {
1848 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1849
            } else {
1850 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1851
1852 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1853 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1854
                }
1855
            }
1856
        }
1857
1858
        // Mark the managed copy visited as well
1859 11
        $visited[spl_object_hash($managedCopy)] = $managedCopy;
1860
1861 11
        $this->cascadeMerge($document, $managedCopy, $visited);
1862
1863 11
        return $managedCopy;
1864
    }
1865
1866
    /**
1867
     * Detaches a document from the persistence management. It's persistence will
1868
     * no longer be managed by Doctrine.
1869
     *
1870
     * @internal
1871
     */
1872 11
    public function detach(object $document) : void
1873
    {
1874 11
        $visited = [];
1875 11
        $this->doDetach($document, $visited);
1876 11
    }
1877
1878
    /**
1879
     * Executes a detach operation on the given document.
1880
     *
1881
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1882
     */
1883 17
    private function doDetach(object $document, array &$visited) : void
1884
    {
1885 17
        $oid = spl_object_hash($document);
1886 17
        if (isset($visited[$oid])) {
1887 3
            return; // Prevent infinite recursion
1888
        }
1889
1890 17
        $visited[$oid] = $document; // mark visited
1891
1892 17
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1893 17
            case self::STATE_MANAGED:
1894 17
                $this->removeFromIdentityMap($document);
1895
                unset(
1896 17
                    $this->documentInsertions[$oid],
1897 17
                    $this->documentUpdates[$oid],
1898 17
                    $this->documentDeletions[$oid],
1899 17
                    $this->documentIdentifiers[$oid],
1900 17
                    $this->documentStates[$oid],
1901 17
                    $this->originalDocumentData[$oid],
1902 17
                    $this->parentAssociations[$oid],
1903 17
                    $this->documentUpserts[$oid],
1904 17
                    $this->hasScheduledCollections[$oid],
1905 17
                    $this->embeddedDocumentsRegistry[$oid]
1906
                );
1907 17
                break;
1908 3
            case self::STATE_NEW:
1909 3
            case self::STATE_DETACHED:
1910 3
                return;
1911
        }
1912
1913 17
        $this->cascadeDetach($document, $visited);
1914 17
    }
1915
1916
    /**
1917
     * Refreshes the state of the given document from the database, overwriting
1918
     * any local, unpersisted changes.
1919
     *
1920
     * @internal
1921
     *
1922
     * @throws InvalidArgumentException If the document is not MANAGED.
1923
     */
1924 24
    public function refresh(object $document) : void
1925
    {
1926 24
        $visited = [];
1927 24
        $this->doRefresh($document, $visited);
1928 23
    }
1929
1930
    /**
1931
     * Executes a refresh operation on a document.
1932
     *
1933
     * @throws InvalidArgumentException If the document is not MANAGED.
1934
     */
1935 24
    private function doRefresh(object $document, array &$visited) : void
1936
    {
1937 24
        $oid = spl_object_hash($document);
1938 24
        if (isset($visited[$oid])) {
1939
            return; // Prevent infinite recursion
1940
        }
1941
1942 24
        $visited[$oid] = $document; // mark visited
1943
1944 24
        $class = $this->dm->getClassMetadata(get_class($document));
1945
1946 24
        if (! $class->isEmbeddedDocument) {
1947 24
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
1948 1
                throw new InvalidArgumentException('Document is not MANAGED.');
1949
            }
1950
1951 23
            $this->getDocumentPersister($class->name)->refresh($document);
1952
        }
1953
1954 23
        $this->cascadeRefresh($document, $visited);
1955 23
    }
1956
1957
    /**
1958
     * Cascades a refresh operation to associated documents.
1959
     */
1960 23
    private function cascadeRefresh(object $document, array &$visited) : void
1961
    {
1962 23
        $class = $this->dm->getClassMetadata(get_class($document));
1963
1964 23
        $associationMappings = array_filter(
1965 23
            $class->associationMappings,
1966
            static function ($assoc) {
1967 18
                return $assoc['isCascadeRefresh'];
1968 23
            }
1969
        );
1970
1971 23
        foreach ($associationMappings as $mapping) {
1972 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1973 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...
1974 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
1975
                    // Unwrap so that foreach() does not initialize
1976 15
                    $relatedDocuments = $relatedDocuments->unwrap();
1977
                }
1978 15
                foreach ($relatedDocuments as $relatedDocument) {
1979
                    $this->doRefresh($relatedDocument, $visited);
1980
                }
1981 10
            } elseif ($relatedDocuments !== null) {
1982 2
                $this->doRefresh($relatedDocuments, $visited);
1983
            }
1984
        }
1985 23
    }
1986
1987
    /**
1988
     * Cascades a detach operation to associated documents.
1989
     */
1990 17
    private function cascadeDetach(object $document, array &$visited) : void
1991
    {
1992 17
        $class = $this->dm->getClassMetadata(get_class($document));
1993 17
        foreach ($class->fieldMappings as $mapping) {
1994 17
            if (! $mapping['isCascadeDetach']) {
1995 17
                continue;
1996
            }
1997 11
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
1998 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...
1999 11
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2000
                    // Unwrap so that foreach() does not initialize
2001 8
                    $relatedDocuments = $relatedDocuments->unwrap();
2002
                }
2003 11
                foreach ($relatedDocuments as $relatedDocument) {
2004 8
                    $this->doDetach($relatedDocument, $visited);
2005
                }
2006 11
            } elseif ($relatedDocuments !== null) {
2007 8
                $this->doDetach($relatedDocuments, $visited);
2008
            }
2009
        }
2010 17
    }
2011
    /**
2012
     * Cascades a merge operation to associated documents.
2013
     */
2014 11
    private function cascadeMerge(object $document, object $managedCopy, array &$visited) : void
2015
    {
2016 11
        $class = $this->dm->getClassMetadata(get_class($document));
2017
2018 11
        $associationMappings = array_filter(
2019 11
            $class->associationMappings,
2020
            static function ($assoc) {
2021 11
                return $assoc['isCascadeMerge'];
2022 11
            }
2023
        );
2024
2025 11
        foreach ($associationMappings as $assoc) {
2026 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2027
2028 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...
2029 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2030
                    // Collections are the same, so there is nothing to do
2031 1
                    continue;
2032
                }
2033
2034 8
                foreach ($relatedDocuments as $relatedDocument) {
2035 4
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2036
                }
2037 6
            } elseif ($relatedDocuments !== null) {
2038 4
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2039
            }
2040
        }
2041 11
    }
2042
2043
    /**
2044
     * Cascades the save operation to associated documents.
2045
     */
2046 641
    private function cascadePersist(object $document, array &$visited) : void
2047
    {
2048 641
        $class = $this->dm->getClassMetadata(get_class($document));
2049
2050 641
        $associationMappings = array_filter(
2051 641
            $class->associationMappings,
2052
            static function ($assoc) {
2053 496
                return $assoc['isCascadePersist'];
2054 641
            }
2055
        );
2056
2057 641
        foreach ($associationMappings as $fieldName => $mapping) {
2058 444
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2059
2060 444
            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...
2061 372
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2062 15
                    if ($relatedDocuments->getOwner() !== $document) {
2063 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2064
                    }
2065
                    // Unwrap so that foreach() does not initialize
2066 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2067
                }
2068
2069 372
                $count = 0;
2070 372
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2071 201
                    if (! empty($mapping['embedded'])) {
2072 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...
2073 130
                        if ($knownParent && $knownParent !== $document) {
2074 1
                            $relatedDocument               = clone $relatedDocument;
2075 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2076
                        }
2077 130
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2078 130
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2079
                    }
2080 201
                    $this->doPersist($relatedDocument, $visited);
2081
                }
2082 347
            } elseif ($relatedDocuments !== null) {
2083 128
                if (! empty($mapping['embedded'])) {
2084 69
                    [, $knownParent ] = $this->getParentAssociation($relatedDocuments);
2085 69
                    if ($knownParent && $knownParent !== $document) {
2086 3
                        $relatedDocuments = clone $relatedDocuments;
2087 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2088
                    }
2089 69
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2090
                }
2091 128
                $this->doPersist($relatedDocuments, $visited);
2092
            }
2093
        }
2094 639
    }
2095
2096
    /**
2097
     * Cascades the delete operation to associated documents.
2098
     */
2099 83
    private function cascadeRemove(object $document, array &$visited) : void
2100
    {
2101 83
        $class = $this->dm->getClassMetadata(get_class($document));
2102 83
        foreach ($class->fieldMappings as $mapping) {
2103 83
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2104 82
                continue;
2105
            }
2106 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...
2107 3
                $document->initializeProxy();
2108
            }
2109
2110 43
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2111 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...
2112
                // If its a PersistentCollection initialization is intended! No unwrap!
2113 31
                foreach ($relatedDocuments as $relatedDocument) {
2114 15
                    $this->doRemove($relatedDocument, $visited);
2115
                }
2116 27
            } elseif ($relatedDocuments !== null) {
2117 14
                $this->doRemove($relatedDocuments, $visited);
2118
            }
2119
        }
2120 83
    }
2121
2122
    /**
2123
     * Acquire a lock on the given document.
2124
     *
2125
     * @internal
2126
     *
2127
     * @throws LockException
2128
     * @throws InvalidArgumentException
2129
     */
2130 8
    public function lock(object $document, int $lockMode, ?int $lockVersion = null) : void
2131
    {
2132 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2133 1
            throw new InvalidArgumentException('Document is not MANAGED.');
2134
        }
2135
2136 7
        $documentName = get_class($document);
2137 7
        $class        = $this->dm->getClassMetadata($documentName);
2138
2139 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2140 2
            if (! $class->isVersioned) {
2141 1
                throw LockException::notVersioned($documentName);
2142
            }
2143
2144 1
            if ($lockVersion !== null) {
2145 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2146 1
                if ($documentVersion !== $lockVersion) {
2147 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2148
                }
2149
            }
2150 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2151 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2152
        }
2153 5
    }
2154
2155
    /**
2156
     * Releases a lock on the given document.
2157
     *
2158
     * @internal
2159
     *
2160
     * @throws InvalidArgumentException
2161
     */
2162 1
    public function unlock(object $document) : void
2163
    {
2164 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2165
            throw new InvalidArgumentException('Document is not MANAGED.');
2166
        }
2167 1
        $documentName = get_class($document);
2168 1
        $this->getDocumentPersister($documentName)->unlock($document);
2169 1
    }
2170
2171
    /**
2172
     * Clears the UnitOfWork.
2173
     *
2174
     * @internal
2175
     */
2176 379
    public function clear(?string $documentName = null) : void
2177
    {
2178 379
        if ($documentName === null) {
2179 373
            $this->identityMap                 =
2180 373
            $this->documentIdentifiers         =
2181 373
            $this->originalDocumentData        =
2182 373
            $this->documentChangeSets          =
2183 373
            $this->documentStates              =
2184 373
            $this->scheduledForSynchronization =
2185 373
            $this->documentInsertions          =
2186 373
            $this->documentUpserts             =
2187 373
            $this->documentUpdates             =
2188 373
            $this->documentDeletions           =
2189 373
            $this->collectionUpdates           =
2190 373
            $this->collectionDeletions         =
2191 373
            $this->parentAssociations          =
2192 373
            $this->embeddedDocumentsRegistry   =
2193 373
            $this->orphanRemovals              =
2194 373
            $this->hasScheduledCollections     = [];
2195
        } else {
2196 6
            $visited = [];
2197 6
            foreach ($this->identityMap as $className => $documents) {
2198 6
                if ($className !== $documentName) {
2199 3
                    continue;
2200
                }
2201
2202 6
                foreach ($documents as $document) {
2203 6
                    $this->doDetach($document, $visited);
2204
                }
2205
            }
2206
        }
2207
2208 379
        if (! $this->evm->hasListeners(Events::onClear)) {
2209 379
            return;
2210
        }
2211
2212
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2213
    }
2214
2215
    /**
2216
     * Schedules an embedded document for removal. The remove() operation will be
2217
     * invoked on that document at the beginning of the next commit of this
2218
     * UnitOfWork.
2219
     *
2220
     * @internal
2221
     */
2222 58
    public function scheduleOrphanRemoval(object $document) : void
2223
    {
2224 58
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2225 58
    }
2226
2227
    /**
2228
     * Unschedules an embedded or referenced object for removal.
2229
     *
2230
     * @internal
2231
     */
2232 123
    public function unscheduleOrphanRemoval(object $document) : void
2233
    {
2234 123
        $oid = spl_object_hash($document);
2235 123
        unset($this->orphanRemovals[$oid]);
2236 123
    }
2237
2238
    /**
2239
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2240
     *  1) sets owner if it was cloned
2241
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2242
     *  3) NOP if state is OK
2243
     * Returned collection should be used from now on (only important with 2nd point)
2244
     */
2245 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, object $document, ClassMetadata $class, string $propName) : PersistentCollectionInterface
2246
    {
2247 8
        $owner = $coll->getOwner();
2248 8
        if ($owner === null) { // cloned
2249 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2250 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2251 2
            if (! $coll->isInitialized()) {
2252 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2253
            }
2254 2
            $newValue = clone $coll;
2255 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2256 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2257 2
            if ($this->isScheduledForUpdate($document)) {
2258
                // @todo following line should be superfluous once collections are stored in change sets
2259
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2260
            }
2261 2
            return $newValue;
2262
        }
2263 6
        return $coll;
2264
    }
2265
2266
    /**
2267
     * Schedules a complete collection for removal when this UnitOfWork commits.
2268
     *
2269
     * @internal
2270
     */
2271 47
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2272
    {
2273 47
        $oid = spl_object_hash($coll);
2274 47
        unset($this->collectionUpdates[$oid]);
2275 47
        if (isset($this->collectionDeletions[$oid])) {
2276
            return;
2277
        }
2278
2279 47
        $this->collectionDeletions[$oid] = $coll;
2280 47
        $this->scheduleCollectionOwner($coll);
2281 47
    }
2282
2283
    /**
2284
     * Checks whether a PersistentCollection is scheduled for deletion.
2285
     *
2286
     * @internal
2287
     */
2288 220
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll) : bool
2289
    {
2290 220
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2291
    }
2292
2293
    /**
2294
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2295
     *
2296
     * @internal
2297
     */
2298 227
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll) : void
2299
    {
2300 227
        if ($coll->getOwner() === null) {
2301
            return;
2302
        }
2303
2304 227
        $oid = spl_object_hash($coll);
2305 227
        if (! isset($this->collectionDeletions[$oid])) {
2306 227
            return;
2307
        }
2308
2309 14
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2310 14
        unset($this->collectionDeletions[$oid]);
2311 14
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2312 14
    }
2313
2314
    /**
2315
     * Schedules a collection for update when this UnitOfWork commits.
2316
     *
2317
     * @internal
2318
     */
2319 253
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2320
    {
2321 253
        $mapping = $coll->getMapping();
2322 253
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2323
            /* There is no need to $unset collection if it will be $set later
2324
             * This is NOP if collection is not scheduled for deletion
2325
             */
2326 44
            $this->unscheduleCollectionDeletion($coll);
2327
        }
2328 253
        $oid = spl_object_hash($coll);
2329 253
        if (isset($this->collectionUpdates[$oid])) {
2330 11
            return;
2331
        }
2332
2333 253
        $this->collectionUpdates[$oid] = $coll;
2334 253
        $this->scheduleCollectionOwner($coll);
2335 253
    }
2336
2337
    /**
2338
     * Unschedules a collection from being updated when this UnitOfWork commits.
2339
     *
2340
     * @internal
2341
     */
2342 227
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll) : void
2343
    {
2344 227
        if ($coll->getOwner() === null) {
2345
            return;
2346
        }
2347
2348 227
        $oid = spl_object_hash($coll);
2349 227
        if (! isset($this->collectionUpdates[$oid])) {
2350 52
            return;
2351
        }
2352
2353 216
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2354 216
        unset($this->collectionUpdates[$oid]);
2355 216
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2356 216
    }
2357
2358
    /**
2359
     * Checks whether a PersistentCollection is scheduled for update.
2360
     *
2361
     * @internal
2362
     */
2363 140
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll) : bool
2364
    {
2365 140
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2366
    }
2367
2368
    /**
2369
     * Gets PersistentCollections that have been visited during computing change
2370
     * set of $document
2371
     *
2372
     * @internal
2373
     *
2374
     * @return PersistentCollectionInterface[]
2375
     */
2376 593
    public function getVisitedCollections(object $document) : array
2377
    {
2378 593
        $oid = spl_object_hash($document);
2379
2380 593
        return $this->visitedCollections[$oid] ?? [];
2381
    }
2382
2383
    /**
2384
     * Gets PersistentCollections that are scheduled to update and related to $document
2385
     *
2386
     * @internal
2387
     *
2388
     * @return PersistentCollectionInterface[]
2389
     */
2390 593
    public function getScheduledCollections(object $document) : array
2391
    {
2392 593
        $oid = spl_object_hash($document);
2393
2394 593
        return $this->hasScheduledCollections[$oid] ?? [];
2395
    }
2396
2397
    /**
2398
     * Checks whether the document is related to a PersistentCollection
2399
     * scheduled for update or deletion.
2400
     *
2401
     * @internal
2402
     */
2403 57
    public function hasScheduledCollections(object $document) : bool
2404
    {
2405 57
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2406
    }
2407
2408
    /**
2409
     * Marks the PersistentCollection's top-level owner as having a relation to
2410
     * a collection scheduled for update or deletion.
2411
     *
2412
     * If the owner is not scheduled for any lifecycle action, it will be
2413
     * scheduled for update to ensure that versioning takes place if necessary.
2414
     *
2415
     * If the collection is nested within atomic collection, it is immediately
2416
     * unscheduled and atomic one is scheduled for update instead. This makes
2417
     * calculating update data way easier.
2418
     */
2419 255
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll) : void
2420
    {
2421 255
        if ($coll->getOwner() === null) {
2422
            return;
2423
        }
2424
2425 255
        $document                                                                          = $this->getOwningDocument($coll->getOwner());
2426 255
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2427
2428 255
        if ($document !== $coll->getOwner()) {
2429 26
            $parent  = $coll->getOwner();
2430 26
            $mapping = [];
2431 26
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2432 26
                [$mapping, $parent ] = $parentAssoc;
2433
            }
2434 26
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2435 8
                $class            = $this->dm->getClassMetadata(get_class($document));
2436 8
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2437 8
                $this->scheduleCollectionUpdate($atomicCollection);
2438 8
                $this->unscheduleCollectionDeletion($coll);
2439 8
                $this->unscheduleCollectionUpdate($coll);
2440
            }
2441
        }
2442
2443 255
        if ($this->isDocumentScheduled($document)) {
2444 250
            return;
2445
        }
2446
2447 55
        $this->scheduleForUpdate($document);
2448 55
    }
2449
2450
    /**
2451
     * Get the top-most owning document of a given document
2452
     *
2453
     * If a top-level document is provided, that same document will be returned.
2454
     * For an embedded document, we will walk through parent associations until
2455
     * we find a top-level document.
2456
     *
2457
     * @throws UnexpectedValueException When a top-level document could not be found.
2458
     */
2459 257
    public function getOwningDocument(object $document) : object
2460
    {
2461 257
        $class = $this->dm->getClassMetadata(get_class($document));
2462 257
        while ($class->isEmbeddedDocument) {
2463 42
            $parentAssociation = $this->getParentAssociation($document);
2464
2465 42
            if (! $parentAssociation) {
2466
                throw new UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2467
            }
2468
2469 42
            [, $document ] = $parentAssociation;
2470 42
            $class         = $this->dm->getClassMetadata(get_class($document));
2471
        }
2472
2473 257
        return $document;
2474
    }
2475
2476
    /**
2477
     * Gets the class name for an association (embed or reference) with respect
2478
     * to any discriminator value.
2479
     *
2480
     * @internal
2481
     *
2482
     * @param array|null $data
2483
     */
2484 228
    public function getClassNameForAssociation(array $mapping, $data) : string
2485
    {
2486 228
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2487
2488 228
        $discriminatorValue = null;
2489 228
        if (isset($discriminatorField, $data[$discriminatorField])) {
2490 13
            $discriminatorValue = $data[$discriminatorField];
2491 216
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2492
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2493
        }
2494
2495 228
        if ($discriminatorValue !== null) {
2496 13
            return $mapping['discriminatorMap'][$discriminatorValue]
2497 13
                ?? (string) $discriminatorValue;
2498
        }
2499
2500 216
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2501
2502 216
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2503 11
            $discriminatorValue = $data[$class->discriminatorField];
2504 206
        } elseif ($class->defaultDiscriminatorValue !== null) {
2505 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2506
        }
2507
2508 216
        if ($discriminatorValue !== null) {
2509 12
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2510
        }
2511
2512 205
        return $mapping['targetDocument'];
2513
    }
2514
2515
    /**
2516
     * Creates a document. Used for reconstitution of documents during hydration.
2517
     *
2518
     * @internal
2519
     */
2520 403
    public function getOrCreateDocument(string $className, array $data, array &$hints = [], ?object $document = null) : object
2521
    {
2522 403
        $class = $this->dm->getClassMetadata($className);
2523
2524
        // @TODO figure out how to remove this
2525 403
        $discriminatorValue = null;
2526 403
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2527 15
            $discriminatorValue = $data[$class->discriminatorField];
2528 395
        } elseif (isset($class->defaultDiscriminatorValue)) {
2529 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2530
        }
2531
2532 403
        if ($discriminatorValue !== null) {
2533 16
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2534
2535 16
            $class = $this->dm->getClassMetadata($className);
2536
2537 16
            unset($data[$class->discriminatorField]);
2538
        }
2539
2540 403
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2541 2
            $document = $class->newInstance();
2542 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2543 2
            return $document;
2544
        }
2545
2546 402
        $isManagedObject = false;
2547 402
        $serializedId    = null;
2548 402
        $id              = null;
2549 402
        if (! $class->isQueryResultDocument) {
2550 399
            $id              = $class->getDatabaseIdentifierValue($data['_id']);
2551 399
            $serializedId    = serialize($id);
2552 399
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2553
        }
2554
2555 402
        $oid = null;
2556 402
        if ($isManagedObject) {
2557 101
            $document = $this->identityMap[$class->name][$serializedId];
2558 101
            $oid      = spl_object_hash($document);
2559 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...
2560 16
                $document->setProxyInitializer(null);
2561 16
                $overrideLocalValues = true;
2562 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...
2563 16
                    $document->addPropertyChangedListener($this);
2564
                }
2565
            } else {
2566 91
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2567
            }
2568 101
            if ($overrideLocalValues) {
2569 42
                $data                             = $this->hydratorFactory->hydrate($document, $data, $hints);
2570 101
                $this->originalDocumentData[$oid] = $data;
2571
            }
2572
        } else {
2573 353
            if ($document === null) {
2574 353
                $document = $class->newInstance();
2575
            }
2576
2577 353
            if (! $class->isQueryResultDocument) {
2578 349
                $this->registerManaged($document, $id, $data);
2579 349
                $oid                                            = spl_object_hash($document);
2580 349
                $this->documentStates[$oid]                     = self::STATE_MANAGED;
2581 349
                $this->identityMap[$class->name][$serializedId] = $document;
2582
            }
2583
2584 353
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2585
2586 353
            if (! $class->isQueryResultDocument) {
2587 349
                $this->originalDocumentData[$oid] = $data;
2588
            }
2589
        }
2590
2591 402
        return $document;
2592
    }
2593
2594
    /**
2595
     * Initializes (loads) an uninitialized persistent collection of a document.
2596
     *
2597
     * @internal
2598
     */
2599 178
    public function loadCollection(PersistentCollectionInterface $collection) : void
2600
    {
2601 178
        if ($collection->getOwner() === null) {
2602
            throw PersistentCollectionException::ownerRequiredToLoadCollection();
2603
        }
2604
2605 178
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2606 178
        $this->lifecycleEventManager->postCollectionLoad($collection);
2607 178
    }
2608
2609
    /**
2610
     * Gets the identity map of the UnitOfWork.
2611
     *
2612
     * @internal
2613
     */
2614
    public function getIdentityMap() : array
2615
    {
2616
        return $this->identityMap;
2617
    }
2618
2619
    /**
2620
     * Gets the original data of a document. The original data is the data that was
2621
     * present at the time the document was reconstituted from the database.
2622
     *
2623
     * @return array
2624
     */
2625 1
    public function getOriginalDocumentData(object $document) : array
2626
    {
2627 1
        $oid = spl_object_hash($document);
2628
2629 1
        return $this->originalDocumentData[$oid] ?? [];
2630
    }
2631
2632
    /**
2633
     * @internal
2634
     */
2635 60
    public function setOriginalDocumentData(object $document, array $data) : void
2636
    {
2637 60
        $oid                              = spl_object_hash($document);
2638 60
        $this->originalDocumentData[$oid] = $data;
2639 60
        unset($this->documentChangeSets[$oid]);
2640 60
    }
2641
2642
    /**
2643
     * Sets a property value of the original data array of a document.
2644
     *
2645
     * @internal
2646
     *
2647
     * @param mixed $value
2648
     */
2649 3
    public function setOriginalDocumentProperty(string $oid, string $property, $value) : void
2650
    {
2651 3
        $this->originalDocumentData[$oid][$property] = $value;
2652 3
    }
2653
2654
    /**
2655
     * Gets the identifier of a document.
2656
     *
2657
     * @return mixed The identifier value
2658
     */
2659 465
    public function getDocumentIdentifier(object $document)
2660
    {
2661 465
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2662
    }
2663
2664
    /**
2665
     * Checks whether the UnitOfWork has any pending insertions.
2666
     *
2667
     * @internal
2668
     *
2669
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2670
     */
2671
    public function hasPendingInsertions() : bool
2672
    {
2673
        return ! empty($this->documentInsertions);
2674
    }
2675
2676
    /**
2677
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2678
     * number of documents in the identity map.
2679
     *
2680
     * @internal
2681
     */
2682 2
    public function size() : int
2683
    {
2684 2
        $count = 0;
2685 2
        foreach ($this->identityMap as $documentSet) {
2686 2
            $count += count($documentSet);
2687
        }
2688 2
        return $count;
2689
    }
2690
2691
    /**
2692
     * Registers a document as managed.
2693
     *
2694
     * TODO: This method assumes that $id is a valid PHP identifier for the
2695
     * document class. If the class expects its database identifier to be an
2696
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2697
     * document identifiers map will become inconsistent with the identity map.
2698
     * In the future, we may want to round-trip $id through a PHP and database
2699
     * conversion and throw an exception if it's inconsistent.
2700
     *
2701
     * @internal
2702
     *
2703
     * @param mixed $id The identifier values.
2704
     */
2705 382
    public function registerManaged(object $document, $id, array $data) : void
2706
    {
2707 382
        $oid   = spl_object_hash($document);
2708 382
        $class = $this->dm->getClassMetadata(get_class($document));
2709
2710 382
        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...
2711 110
            $this->documentIdentifiers[$oid] = $oid;
2712
        } else {
2713 376
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2714
        }
2715
2716 382
        $this->documentStates[$oid]       = self::STATE_MANAGED;
2717 382
        $this->originalDocumentData[$oid] = $data;
2718 382
        $this->addToIdentityMap($document);
2719 382
    }
2720
2721
    /**
2722
     * Clears the property changeset of the document with the given OID.
2723
     *
2724
     * @internal
2725
     */
2726
    public function clearDocumentChangeSet(string $oid)
2727
    {
2728
        $this->documentChangeSets[$oid] = [];
2729
    }
2730
2731
    /* PropertyChangedListener implementation */
2732
2733
    /**
2734
     * Notifies this UnitOfWork of a property change in a document.
2735
     *
2736
     * @param object $document     The document that owns the property.
2737
     * @param string $propertyName The name of the property that changed.
2738
     * @param mixed  $oldValue     The old value of the property.
2739
     * @param mixed  $newValue     The new value of the property.
2740
     */
2741 1
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2742
    {
2743 1
        $oid   = spl_object_hash($document);
2744 1
        $class = $this->dm->getClassMetadata(get_class($document));
2745
2746 1
        if (! isset($class->fieldMappings[$propertyName])) {
2747
            return; // ignore non-persistent fields
2748
        }
2749
2750
        // Update changeset and mark document for synchronization
2751 1
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2752 1
        if (isset($this->scheduledForSynchronization[$class->name][$oid])) {
2753
            return;
2754
        }
2755
2756 1
        $this->scheduleForSynchronization($document);
2757 1
    }
2758
2759
    /**
2760
     * Gets the currently scheduled document insertions in this UnitOfWork.
2761
     */
2762 3
    public function getScheduledDocumentInsertions() : array
2763
    {
2764 3
        return $this->documentInsertions;
2765
    }
2766
2767
    /**
2768
     * Gets the currently scheduled document upserts in this UnitOfWork.
2769
     */
2770 1
    public function getScheduledDocumentUpserts() : array
2771
    {
2772 1
        return $this->documentUpserts;
2773
    }
2774
2775
    /**
2776
     * Gets the currently scheduled document updates in this UnitOfWork.
2777
     */
2778 2
    public function getScheduledDocumentUpdates() : array
2779
    {
2780 2
        return $this->documentUpdates;
2781
    }
2782
2783
    /**
2784
     * Gets the currently scheduled document deletions in this UnitOfWork.
2785
     */
2786
    public function getScheduledDocumentDeletions() : array
2787
    {
2788
        return $this->documentDeletions;
2789
    }
2790
2791
    /**
2792
     * Get the currently scheduled complete collection deletions
2793
     *
2794
     * @internal
2795
     */
2796
    public function getScheduledCollectionDeletions() : array
2797
    {
2798
        return $this->collectionDeletions;
2799
    }
2800
2801
    /**
2802
     * Gets the currently scheduled collection inserts, updates and deletes.
2803
     *
2804
     * @internal
2805
     */
2806
    public function getScheduledCollectionUpdates() : array
2807
    {
2808
        return $this->collectionUpdates;
2809
    }
2810
2811
    /**
2812
     * Helper method to initialize a lazy loading proxy or persistent collection.
2813
     *
2814
     * @internal
2815
     */
2816
    public function initializeObject(object $obj) : void
2817
    {
2818
        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...
2819
            $obj->initializeProxy();
2820
        } elseif ($obj instanceof PersistentCollectionInterface) {
2821
            $obj->initialize();
2822
        }
2823
    }
2824
2825
    private function objToStr(object $obj) : string
2826
    {
2827
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2828
    }
2829
}
2830