Completed
Pull Request — master (#1790)
by Andreas
17:21
created

UnitOfWork::addToIdentityMap()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
860 10
                    continue;
861
                }
862
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
863 572
                $oid = spl_object_hash($document);
864 572
                if (isset($this->documentInsertions[$oid])
865 305
                    || isset($this->documentUpserts[$oid])
866 261
                    || isset($this->documentDeletions[$oid])
867 572
                    || ! isset($this->documentStates[$oid])
868
                ) {
869 570
                    continue;
870
                }
871
872 577
                $this->computeChangeSet($class, $document);
873
            }
874
        }
875 577
    }
876
877
    /**
878
     * Computes the changes of an association.
879
     *
880
     * @param object $parentDocument
881
     * @param array  $assoc
882
     * @param mixed  $value          The value of the association.
883
     * @throws \InvalidArgumentException
884
     */
885 425
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
886
    {
887 425
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
888 425
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
889 425
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
890
891 425
        if ($value instanceof Proxy && ! $value->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
892 7
            return;
893
        }
894
895 424
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
896 230
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
897 226
                $this->scheduleCollectionUpdate($value);
898
            }
899 230
            $topmostOwner = $this->getOwningDocument($value->getOwner());
900 230
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
901 230
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
902 124
                $value->initialize();
903 124
                foreach ($value->getDeletedDocuments() as $orphan) {
904 20
                    $this->scheduleOrphanRemoval($orphan);
905
                }
906
            }
907
        }
908
909
        // Look through the documents, and in any of their associations,
910
        // for transient (new) documents, recursively. ("Persistence by reachability")
911
        // Unwrap. Uninitialized collections will simply be empty.
912 424
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap();
913
914 424
        $count = 0;
915 424
        foreach ($unwrappedValue as $key => $entry) {
916 340
            if (! is_object($entry)) {
917 1
                throw new \InvalidArgumentException(
918 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
919
                );
920
            }
921
922 339
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
923
924 339
            $state = $this->getDocumentState($entry, self::STATE_NEW);
925
926
            // Handle "set" strategy for multi-level hierarchy
927 339
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
928 339
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
929
930 339
            $count++;
931
932
            switch ($state) {
933 339
                case self::STATE_NEW:
934 54
                    if (! $assoc['isCascadePersist']) {
935
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
936
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
937
                            . ' Explicitly persist the new document or configure cascading persist operations'
938
                            . ' on the relationship.');
939
                    }
940
941 54
                    $this->persistNew($targetClass, $entry);
942 54
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
943 54
                    $this->computeChangeSet($targetClass, $entry);
944 54
                    break;
945
946 335
                case self::STATE_MANAGED:
947 335
                    if ($targetClass->isEmbeddedDocument) {
948 152
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
949 152
                        if ($knownParent && $knownParent !== $parentDocument) {
950 6
                            $entry = clone $entry;
951 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
952 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
953 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
954 3
                                $poid = spl_object_hash($parentDocument);
955 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
956 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
957
                                }
958
                            } else {
959
                                // must use unwrapped value to not trigger orphan removal
960 4
                                $unwrappedValue[$key] = $entry;
961
                            }
962 6
                            $this->persistNew($targetClass, $entry);
963
                        }
964 152
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
965 152
                        $this->computeChangeSet($targetClass, $entry);
966
                    }
967 335
                    break;
968
969 1
                case self::STATE_REMOVED:
970
                    // Consume the $value as array (it's either an array or an ArrayAccess)
971
                    // and remove the element from Collection.
972 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
973
                        unset($value[$key]);
974
                    }
975 1
                    break;
976
977
                case self::STATE_DETACHED:
978
                    // Can actually not happen right now as we assume STATE_NEW,
979
                    // so the exception will be raised from the DBAL layer (constraint violation).
980
                    throw new \InvalidArgumentException('A detached document was found through a '
981
                        . 'relationship during cascading a persist operation.');
982
983 339
                default:
984
                    // MANAGED associated documents are already taken into account
985
                    // during changeset calculation anyway, since they are in the identity map.
986
            }
987
        }
988 423
    }
989
990
    /**
991
     * INTERNAL:
992
     * Computes the changeset of an individual document, independently of the
993
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
994
     *
995
     * The passed document must be a managed document. If the document already has a change set
996
     * because this method is invoked during a commit cycle then the change sets are added.
997
     * whereby changes detected in this method prevail.
998
     *
999
     * @ignore
1000
     * @param ClassMetadata $class    The class descriptor of the document.
1001
     * @param object        $document The document for which to (re)calculate the change set.
1002
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
1003
     */
1004 17
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
1005
    {
1006
        // Ignore uninitialized proxy objects
1007 17
        if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1008 1
            return;
1009
        }
1010
1011 16
        $oid = spl_object_hash($document);
1012
1013 16
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
1014
            throw new \InvalidArgumentException('Document must be managed.');
1015
        }
1016
1017 16
        if (! $class->isInheritanceTypeNone()) {
1018 1
            $class = $this->dm->getClassMetadata(get_class($document));
1019
        }
1020
1021 16
        $this->computeOrRecomputeChangeSet($class, $document, true);
1022 16
    }
1023
1024
    /**
1025
     * @param object $document
1026
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1027
     */
1028 603
    private function persistNew(ClassMetadata $class, $document)
1029
    {
1030 603
        $this->lifecycleEventManager->prePersist($class, $document);
1031 603
        $oid = spl_object_hash($document);
1032 603
        $upsert = false;
1033 603
        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...
1034 603
            $idValue = $class->getIdentifierValue($document);
1035 603
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1036
1037 603
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1038 3
                throw new \InvalidArgumentException(sprintf(
1039 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1040 3
                    get_class($document)
1041
                ));
1042
            }
1043
1044 602
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1045 1
                throw new \InvalidArgumentException(sprintf(
1046 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1047 1
                    get_class($document)
1048
                ));
1049
            }
1050
1051 601
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1052 524
                $idValue = $class->idGenerator->generate($this->dm, $document);
1053 524
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1054 524
                $class->setIdentifierValue($document, $idValue);
1055
            }
1056
1057 601
            $this->documentIdentifiers[$oid] = $idValue;
1058
        } else {
1059
            // this is for embedded documents without identifiers
1060 132
            $this->documentIdentifiers[$oid] = $oid;
1061
        }
1062
1063 601
        $this->documentStates[$oid] = self::STATE_MANAGED;
1064
1065 601
        if ($upsert) {
1066 87
            $this->scheduleForUpsert($class, $document);
1067
        } else {
1068 532
            $this->scheduleForInsert($class, $document);
1069
        }
1070 601
    }
1071
1072
    /**
1073
     * Executes all document insertions for documents of the specified type.
1074
     *
1075
     * @param array $documents Array of documents to insert
1076
     * @param array $options   Array of options to be used with batchInsert()
1077
     */
1078 494
    private function executeInserts(ClassMetadata $class, array $documents, array $options = [])
1079
    {
1080 494
        $persister = $this->getDocumentPersister($class->name);
1081
1082 494
        foreach ($documents as $oid => $document) {
1083 494
            $persister->addInsert($document);
1084 494
            unset($this->documentInsertions[$oid]);
1085
        }
1086
1087 494
        $persister->executeInserts($options);
1088
1089 493
        foreach ($documents as $document) {
1090 493
            $this->lifecycleEventManager->postPersist($class, $document);
1091
        }
1092 493
    }
1093
1094
    /**
1095
     * Executes all document upserts for documents of the specified type.
1096
     *
1097
     * @param array $documents Array of documents to upsert
1098
     * @param array $options   Array of options to be used with batchInsert()
1099
     */
1100 84
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = [])
1101
    {
1102 84
        $persister = $this->getDocumentPersister($class->name);
1103
1104 84
        foreach ($documents as $oid => $document) {
1105 84
            $persister->addUpsert($document);
1106 84
            unset($this->documentUpserts[$oid]);
1107
        }
1108
1109 84
        $persister->executeUpserts($options);
1110
1111 84
        foreach ($documents as $document) {
1112 84
            $this->lifecycleEventManager->postPersist($class, $document);
1113
        }
1114 84
    }
1115
1116
    /**
1117
     * Executes all document updates for documents of the specified type.
1118
     *
1119
     * @param array $documents Array of documents to update
1120
     * @param array $options   Array of options to be used with update()
1121
     */
1122 206
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = [])
1123
    {
1124 206
        if ($class->isReadOnly) {
1125
            return;
1126
        }
1127
1128 206
        $className = $class->name;
1129 206
        $persister = $this->getDocumentPersister($className);
1130
1131 206
        foreach ($documents as $oid => $document) {
1132 206
            $this->lifecycleEventManager->preUpdate($class, $document);
1133
1134 206
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1135 205
                $persister->update($document, $options);
1136
            }
1137
1138 202
            unset($this->documentUpdates[$oid]);
1139
1140 202
            $this->lifecycleEventManager->postUpdate($class, $document);
1141
        }
1142 202
    }
1143
1144
    /**
1145
     * Executes all document deletions for documents of the specified type.
1146
     *
1147
     * @param array $documents Array of documents to delete
1148
     * @param array $options   Array of options to be used with remove()
1149
     */
1150 67
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = [])
1151
    {
1152 67
        $persister = $this->getDocumentPersister($class->name);
1153
1154 67
        foreach ($documents as $oid => $document) {
1155 67
            if (! $class->isEmbeddedDocument) {
1156 35
                $persister->delete($document, $options);
1157
            }
1158
            unset(
1159 65
                $this->documentDeletions[$oid],
1160 65
                $this->documentIdentifiers[$oid],
1161 65
                $this->originalDocumentData[$oid]
1162
            );
1163
1164
            // Clear snapshot information for any referenced PersistentCollection
1165
            // http://www.doctrine-project.org/jira/browse/MODM-95
1166 65
            foreach ($class->associationMappings as $fieldMapping) {
1167 41
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1168 31
                    continue;
1169
                }
1170
1171 26
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1172 26
                if (! ($value instanceof PersistentCollectionInterface)) {
1173 7
                    continue;
1174
                }
1175
1176 22
                $value->clearSnapshot();
1177
            }
1178
1179
            // Document with this $oid after deletion treated as NEW, even if the $oid
1180
            // is obtained by a new document because the old one went out of scope.
1181 65
            $this->documentStates[$oid] = self::STATE_NEW;
1182
1183 65
            $this->lifecycleEventManager->postRemove($class, $document);
1184
        }
1185 65
    }
1186
1187
    /**
1188
     * Schedules a document for insertion into the database.
1189
     * If the document already has an identifier, it will be added to the
1190
     * identity map.
1191
     *
1192
     * @param object $document The document to schedule for insertion.
1193
     * @throws \InvalidArgumentException
1194
     */
1195 535
    public function scheduleForInsert(ClassMetadata $class, $document)
1196
    {
1197 535
        $oid = spl_object_hash($document);
1198
1199 535
        if (isset($this->documentUpdates[$oid])) {
1200
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1201
        }
1202 535
        if (isset($this->documentDeletions[$oid])) {
1203
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1204
        }
1205 535
        if (isset($this->documentInsertions[$oid])) {
1206
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1207
        }
1208
1209 535
        $this->documentInsertions[$oid] = $document;
1210
1211 535
        if (! isset($this->documentIdentifiers[$oid])) {
1212 3
            return;
1213
        }
1214
1215 532
        $this->addToIdentityMap($document);
1216 532
    }
1217
1218
    /**
1219
     * Schedules a document for upsert into the database and adds it to the
1220
     * identity map
1221
     *
1222
     * @param object $document The document to schedule for upsert.
1223
     * @throws \InvalidArgumentException
1224
     */
1225 90
    public function scheduleForUpsert(ClassMetadata $class, $document)
1226
    {
1227 90
        $oid = spl_object_hash($document);
1228
1229 90
        if ($class->isEmbeddedDocument) {
1230
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1231
        }
1232 90
        if (isset($this->documentUpdates[$oid])) {
1233
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1234
        }
1235 90
        if (isset($this->documentDeletions[$oid])) {
1236
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1237
        }
1238 90
        if (isset($this->documentUpserts[$oid])) {
1239
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1240
        }
1241
1242 90
        $this->documentUpserts[$oid] = $document;
1243 90
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1244 90
        $this->addToIdentityMap($document);
1245 90
    }
1246
1247
    /**
1248
     * Checks whether a document is scheduled for insertion.
1249
     *
1250
     * @param object $document
1251
     * @return bool
1252
     */
1253 90
    public function isScheduledForInsert($document)
1254
    {
1255 90
        return isset($this->documentInsertions[spl_object_hash($document)]);
1256
    }
1257
1258
    /**
1259
     * Checks whether a document is scheduled for upsert.
1260
     *
1261
     * @param object $document
1262
     * @return bool
1263
     */
1264 5
    public function isScheduledForUpsert($document)
1265
    {
1266 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1267
    }
1268
1269
    /**
1270
     * Schedules a document for being updated.
1271
     *
1272
     * @param object $document The document to schedule for being updated.
1273
     * @throws \InvalidArgumentException
1274
     */
1275 213
    public function scheduleForUpdate($document)
1276
    {
1277 213
        $oid = spl_object_hash($document);
1278 213
        if (! isset($this->documentIdentifiers[$oid])) {
1279
            throw new \InvalidArgumentException('Document has no identity.');
1280
        }
1281
1282 213
        if (isset($this->documentDeletions[$oid])) {
1283
            throw new \InvalidArgumentException('Document is removed.');
1284
        }
1285
1286 213
        if (isset($this->documentUpdates[$oid])
1287 213
            || isset($this->documentInsertions[$oid])
1288 213
            || isset($this->documentUpserts[$oid])) {
1289 80
            return;
1290
        }
1291
1292 212
        $this->documentUpdates[$oid] = $document;
1293 212
    }
1294
1295
    /**
1296
     * Checks whether a document is registered as dirty in the unit of work.
1297
     * Note: Is not very useful currently as dirty documents are only registered
1298
     * at commit time.
1299
     *
1300
     * @param object $document
1301
     * @return bool
1302
     */
1303 15
    public function isScheduledForUpdate($document)
1304
    {
1305 15
        return isset($this->documentUpdates[spl_object_hash($document)]);
1306
    }
1307
1308 1
    public function isScheduledForDirtyCheck($document)
1309
    {
1310 1
        $class = $this->dm->getClassMetadata(get_class($document));
1311 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1312
    }
1313
1314
    /**
1315
     * INTERNAL:
1316
     * Schedules a document for deletion.
1317
     *
1318
     * @param object $document
1319
     */
1320 72
    public function scheduleForDelete($document)
1321
    {
1322 72
        $oid = spl_object_hash($document);
1323
1324 72
        if (isset($this->documentInsertions[$oid])) {
1325 1
            if ($this->isInIdentityMap($document)) {
1326 1
                $this->removeFromIdentityMap($document);
1327
            }
1328 1
            unset($this->documentInsertions[$oid]);
1329 1
            return; // document has not been persisted yet, so nothing more to do.
1330
        }
1331
1332 71
        if (! $this->isInIdentityMap($document)) {
1333 2
            return; // ignore
1334
        }
1335
1336 70
        $this->removeFromIdentityMap($document);
1337 70
        $this->documentStates[$oid] = self::STATE_REMOVED;
1338
1339 70
        if (isset($this->documentUpdates[$oid])) {
1340
            unset($this->documentUpdates[$oid]);
1341
        }
1342 70
        if (isset($this->documentDeletions[$oid])) {
1343
            return;
1344
        }
1345
1346 70
        $this->documentDeletions[$oid] = $document;
1347 70
    }
1348
1349
    /**
1350
     * Checks whether a document is registered as removed/deleted with the unit
1351
     * of work.
1352
     *
1353
     * @param object $document
1354
     * @return bool
1355
     */
1356 5
    public function isScheduledForDelete($document)
1357
    {
1358 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1359
    }
1360
1361
    /**
1362
     * Checks whether a document is scheduled for insertion, update or deletion.
1363
     *
1364
     * @param object $document
1365
     * @return bool
1366
     */
1367 229
    public function isDocumentScheduled($document)
1368
    {
1369 229
        $oid = spl_object_hash($document);
1370 229
        return isset($this->documentInsertions[$oid]) ||
1371 113
            isset($this->documentUpserts[$oid]) ||
1372 104
            isset($this->documentUpdates[$oid]) ||
1373 229
            isset($this->documentDeletions[$oid]);
1374
    }
1375
1376
    /**
1377
     * INTERNAL:
1378
     * Registers a document in the identity map.
1379
     *
1380
     * Note that documents in a hierarchy are registered with the class name of
1381
     * the root document. Identifiers are serialized before being used as array
1382
     * keys to allow differentiation of equal, but not identical, values.
1383
     *
1384
     * @ignore
1385
     * @param object $document The document to register.
1386
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1387
     *                  the document in question is already managed.
1388
     */
1389 642
    public function addToIdentityMap($document)
1390
    {
1391 642
        $class = $this->dm->getClassMetadata(get_class($document));
1392 642
        $id = $this->getIdForIdentityMap($document);
1393
1394 642
        if (isset($this->identityMap[$class->name][$id])) {
1395 46
            return false;
1396
        }
1397
1398 642
        $this->identityMap[$class->name][$id] = $document;
1399
1400 642
        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...
1401 642
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1402 3
            $document->addPropertyChangedListener($this);
1403
        }
1404
1405 642
        return true;
1406
    }
1407
1408
    /**
1409
     * Gets the state of a document with regard to the current unit of work.
1410
     *
1411
     * @param object   $document
1412
     * @param int|null $assume   The state to assume if the state is not yet known (not MANAGED or REMOVED).
1413
     *                           This parameter can be set to improve performance of document state detection
1414
     *                           by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1415
     *                           is either known or does not matter for the caller of the method.
1416
     * @return int The document state.
1417
     */
1418 607
    public function getDocumentState($document, $assume = null)
1419
    {
1420 607
        $oid = spl_object_hash($document);
1421
1422 607
        if (isset($this->documentStates[$oid])) {
1423 375
            return $this->documentStates[$oid];
1424
        }
1425
1426 606
        $class = $this->dm->getClassMetadata(get_class($document));
1427
1428 606
        if ($class->isEmbeddedDocument) {
1429 165
            return self::STATE_NEW;
1430
        }
1431
1432 603
        if ($assume !== null) {
1433 601
            return $assume;
1434
        }
1435
1436
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1437
         * known. Note that you cannot remember the NEW or DETACHED state in
1438
         * _documentStates since the UoW does not hold references to such
1439
         * objects and the object hash can be reused. More generally, because
1440
         * the state may "change" between NEW/DETACHED without the UoW being
1441
         * aware of it.
1442
         */
1443 3
        $id = $class->getIdentifierObject($document);
1444
1445 3
        if ($id === null) {
1446 2
            return self::STATE_NEW;
1447
        }
1448
1449
        // Check for a version field, if available, to avoid a DB lookup.
1450 2
        if ($class->isVersioned) {
1451
            return $class->getFieldValue($document, $class->versionField)
1452
                ? self::STATE_DETACHED
1453
                : self::STATE_NEW;
1454
        }
1455
1456
        // Last try before DB lookup: check the identity map.
1457 2
        if ($this->tryGetById($id, $class)) {
1458 1
            return self::STATE_DETACHED;
1459
        }
1460
1461
        // DB lookup
1462 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1463 1
            return self::STATE_DETACHED;
1464
        }
1465
1466 1
        return self::STATE_NEW;
1467
    }
1468
1469
    /**
1470
     * INTERNAL:
1471
     * Removes a document from the identity map. This effectively detaches the
1472
     * document from the persistence management of Doctrine.
1473
     *
1474
     * @ignore
1475
     * @param object $document
1476
     * @throws \InvalidArgumentException
1477
     * @return bool
1478
     */
1479 84
    public function removeFromIdentityMap($document)
1480
    {
1481 84
        $oid = spl_object_hash($document);
1482
1483
        // Check if id is registered first
1484 84
        if (! isset($this->documentIdentifiers[$oid])) {
1485
            return false;
1486
        }
1487
1488 84
        $class = $this->dm->getClassMetadata(get_class($document));
1489 84
        $id = $this->getIdForIdentityMap($document);
1490
1491 84
        if (isset($this->identityMap[$class->name][$id])) {
1492 84
            unset($this->identityMap[$class->name][$id]);
1493 84
            $this->documentStates[$oid] = self::STATE_DETACHED;
1494 84
            return true;
1495
        }
1496
1497
        return false;
1498
    }
1499
1500
    /**
1501
     * INTERNAL:
1502
     * Gets a document in the identity map by its identifier hash.
1503
     *
1504
     * @ignore
1505
     * @param mixed         $id    Document identifier
1506
     * @param ClassMetadata $class Document class
1507
     * @return object
1508
     * @throws InvalidArgumentException If the class does not have an identifier.
1509
     */
1510 39
    public function getById($id, ClassMetadata $class)
1511
    {
1512 39
        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...
1513
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1514
        }
1515
1516 39
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1517
1518 39
        return $this->identityMap[$class->name][$serializedId];
1519
    }
1520
1521
    /**
1522
     * INTERNAL:
1523
     * Tries to get a document by its identifier hash. If no document is found
1524
     * for the given hash, FALSE is returned.
1525
     *
1526
     * @ignore
1527
     * @param mixed         $id    Document identifier
1528
     * @param ClassMetadata $class Document class
1529
     * @return mixed The found document or FALSE.
1530
     * @throws InvalidArgumentException If the class does not have an identifier.
1531
     */
1532 303
    public function tryGetById($id, ClassMetadata $class)
1533
    {
1534 303
        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...
1535
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1536
        }
1537
1538 303
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1539
1540 303
        return $this->identityMap[$class->name][$serializedId] ?? false;
1541
    }
1542
1543
    /**
1544
     * Schedules a document for dirty-checking at commit-time.
1545
     *
1546
     * @param object $document The document to schedule for dirty-checking.
1547
     * @todo Rename: scheduleForSynchronization
1548
     */
1549 3
    public function scheduleForDirtyCheck($document)
1550
    {
1551 3
        $class = $this->dm->getClassMetadata(get_class($document));
1552 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1553 3
    }
1554
1555
    /**
1556
     * Checks whether a document is registered in the identity map.
1557
     *
1558
     * @param object $document
1559
     * @return bool
1560
     */
1561 80
    public function isInIdentityMap($document)
1562
    {
1563 80
        $oid = spl_object_hash($document);
1564
1565 80
        if (! isset($this->documentIdentifiers[$oid])) {
1566 6
            return false;
1567
        }
1568
1569 78
        $class = $this->dm->getClassMetadata(get_class($document));
1570 78
        $id = $this->getIdForIdentityMap($document);
1571
1572 78
        return isset($this->identityMap[$class->name][$id]);
1573
    }
1574
1575
    /**
1576
     * @param object $document
1577
     * @return string
1578
     */
1579 642
    private function getIdForIdentityMap($document)
1580
    {
1581 642
        $class = $this->dm->getClassMetadata(get_class($document));
1582
1583 642
        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...
1584 138
            $id = spl_object_hash($document);
1585
        } else {
1586 641
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1587 641
            $id = serialize($class->getDatabaseIdentifierValue($id));
1588
        }
1589
1590 642
        return $id;
1591
    }
1592
1593
    /**
1594
     * INTERNAL:
1595
     * Checks whether an identifier exists in the identity map.
1596
     *
1597
     * @ignore
1598
     * @param string $id
1599
     * @param string $rootClassName
1600
     * @return bool
1601
     */
1602
    public function containsId($id, $rootClassName)
1603
    {
1604
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1605
    }
1606
1607
    /**
1608
     * Persists a document as part of the current unit of work.
1609
     *
1610
     * @param object $document The document to persist.
1611
     * @throws MongoDBException If trying to persist MappedSuperclass.
1612
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1613
     */
1614 604
    public function persist($document)
1615
    {
1616 604
        $class = $this->dm->getClassMetadata(get_class($document));
1617 604
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1618 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1619
        }
1620 603
        $visited = [];
1621 603
        $this->doPersist($document, $visited);
1622 598
    }
1623
1624
    /**
1625
     * Saves a document as part of the current unit of work.
1626
     * This method is internally called during save() cascades as it tracks
1627
     * the already visited documents to prevent infinite recursions.
1628
     *
1629
     * NOTE: This method always considers documents that are not yet known to
1630
     * this UnitOfWork as NEW.
1631
     *
1632
     * @param object $document The document to persist.
1633
     * @param array  $visited  The already visited documents.
1634
     * @throws \InvalidArgumentException
1635
     * @throws MongoDBException
1636
     */
1637 603
    private function doPersist($document, array &$visited)
1638
    {
1639 603
        $oid = spl_object_hash($document);
1640 603
        if (isset($visited[$oid])) {
1641 25
            return; // Prevent infinite recursion
1642
        }
1643
1644 603
        $visited[$oid] = $document; // Mark visited
1645
1646 603
        $class = $this->dm->getClassMetadata(get_class($document));
1647
1648 603
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1649
        switch ($documentState) {
1650 603
            case self::STATE_MANAGED:
1651
                // Nothing to do, except if policy is "deferred explicit"
1652 53
                if ($class->isChangeTrackingDeferredExplicit()) {
1653
                    $this->scheduleForDirtyCheck($document);
1654
                }
1655 53
                break;
1656 603
            case self::STATE_NEW:
1657 603
                if ($class->isFile) {
1658 1
                    throw MongoDBException::cannotPersistGridFSFile($class->name);
1659
                }
1660
1661 602
                $this->persistNew($class, $document);
1662 600
                break;
1663
1664 2
            case self::STATE_REMOVED:
1665
                // Document becomes managed again
1666 2
                unset($this->documentDeletions[$oid]);
1667
1668 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1669 2
                break;
1670
1671
            case self::STATE_DETACHED:
1672
                throw new \InvalidArgumentException(
1673
                    'Behavior of persist() for a detached document is not yet defined.'
1674
                );
1675
1676
            default:
1677
                throw MongoDBException::invalidDocumentState($documentState);
1678
        }
1679
1680 600
        $this->cascadePersist($document, $visited);
1681 598
    }
1682
1683
    /**
1684
     * Deletes a document as part of the current unit of work.
1685
     *
1686
     * @param object $document The document to remove.
1687
     */
1688 71
    public function remove($document)
1689
    {
1690 71
        $visited = [];
1691 71
        $this->doRemove($document, $visited);
1692 71
    }
1693
1694
    /**
1695
     * Deletes a document as part of the current unit of work.
1696
     *
1697
     * This method is internally called during delete() cascades as it tracks
1698
     * the already visited documents to prevent infinite recursions.
1699
     *
1700
     * @param object $document The document to delete.
1701
     * @param array  $visited  The map of the already visited documents.
1702
     * @throws MongoDBException
1703
     */
1704 71
    private function doRemove($document, array &$visited)
1705
    {
1706 71
        $oid = spl_object_hash($document);
1707 71
        if (isset($visited[$oid])) {
1708 1
            return; // Prevent infinite recursion
1709
        }
1710
1711 71
        $visited[$oid] = $document; // mark visited
1712
1713
        /* Cascade first, because scheduleForDelete() removes the entity from
1714
         * the identity map, which can cause problems when a lazy Proxy has to
1715
         * be initialized for the cascade operation.
1716
         */
1717 71
        $this->cascadeRemove($document, $visited);
1718
1719 71
        $class = $this->dm->getClassMetadata(get_class($document));
1720 71
        $documentState = $this->getDocumentState($document);
1721
        switch ($documentState) {
1722 71
            case self::STATE_NEW:
1723 71
            case self::STATE_REMOVED:
1724
                // nothing to do
1725
                break;
1726 71
            case self::STATE_MANAGED:
1727 71
                $this->lifecycleEventManager->preRemove($class, $document);
1728 71
                $this->scheduleForDelete($document);
1729 71
                break;
1730
            case self::STATE_DETACHED:
1731
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1732
            default:
1733
                throw MongoDBException::invalidDocumentState($documentState);
1734
        }
1735 71
    }
1736
1737
    /**
1738
     * Merges the state of the given detached document into this UnitOfWork.
1739
     *
1740
     * @param object $document
1741
     * @return object The managed copy of the document.
1742
     */
1743 12
    public function merge($document)
1744
    {
1745 12
        $visited = [];
1746
1747 12
        return $this->doMerge($document, $visited);
1748
    }
1749
1750
    /**
1751
     * Executes a merge operation on a document.
1752
     *
1753
     * @param object      $document
1754
     * @param array       $visited
1755
     * @param object|null $prevManagedCopy
1756
     * @param array|null  $assoc
1757
     *
1758
     * @return object The managed copy of the document.
1759
     *
1760
     * @throws InvalidArgumentException If the entity instance is NEW.
1761
     * @throws LockException If the document uses optimistic locking through a
1762
     *                       version attribute and the version check against the
1763
     *                       managed copy fails.
1764
     */
1765 12
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1766
    {
1767 12
        $oid = spl_object_hash($document);
1768
1769 12
        if (isset($visited[$oid])) {
1770 1
            return $visited[$oid]; // Prevent infinite recursion
1771
        }
1772
1773 12
        $visited[$oid] = $document; // mark visited
1774
1775 12
        $class = $this->dm->getClassMetadata(get_class($document));
1776
1777
        /* First we assume DETACHED, although it can still be NEW but we can
1778
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1779
         * an identity, we need to fetch it from the DB anyway in order to
1780
         * merge. MANAGED documents are ignored by the merge operation.
1781
         */
1782 12
        $managedCopy = $document;
1783
1784 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1785 12
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1786
                $document->__load();
1787
            }
1788
1789 12
            $identifier = $class->getIdentifier();
1790
            // We always have one element in the identifier array but it might be null
1791 12
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1792 12
            $managedCopy = null;
1793
1794
            // Try to fetch document from the database
1795 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1796 12
                $managedCopy = $this->dm->find($class->name, $id);
1797
1798
                // Managed copy may be removed in which case we can't merge
1799 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1800
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1801
                }
1802
1803 12
                if ($managedCopy instanceof Proxy && ! $managedCopy->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1804
                    $managedCopy->__load();
1805
                }
1806
            }
1807
1808 12
            if ($managedCopy === null) {
1809
                // Create a new managed instance
1810 4
                $managedCopy = $class->newInstance();
1811 4
                if ($id !== null) {
1812 3
                    $class->setIdentifierValue($managedCopy, $id);
1813
                }
1814 4
                $this->persistNew($class, $managedCopy);
1815
            }
1816
1817 12
            if ($class->isVersioned) {
1818
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1819
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1820
1821
                // Throw exception if versions don't match
1822
                if ($managedCopyVersion !== $documentVersion) {
1823
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1824
                }
1825
            }
1826
1827
            // Merge state of $document into existing (managed) document
1828 12
            foreach ($class->reflClass->getProperties() as $prop) {
1829 12
                $name = $prop->name;
1830 12
                $prop->setAccessible(true);
1831 12
                if (! isset($class->associationMappings[$name])) {
1832 12
                    if (! $class->isIdentifier($name)) {
1833 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1834
                    }
1835
                } else {
1836 12
                    $assoc2 = $class->associationMappings[$name];
1837
1838 12
                    if ($assoc2['type'] === 'one') {
1839 6
                        $other = $prop->getValue($document);
1840
1841 6
                        if ($other === null) {
1842 2
                            $prop->setValue($managedCopy, null);
1843 5
                        } elseif ($other instanceof Proxy && ! $other->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1844
                            // Do not merge fields marked lazy that have not been fetched
1845 1
                            continue;
1846 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1847
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1848
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1849
                                /** @var ClassMetadata $targetClass */
1850
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1851
                                $relatedId = $targetClass->getIdentifierObject($other);
1852
1853
                                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...
1854
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1855
                                } else {
1856
                                    $other = $this
1857
                                        ->dm
1858
                                        ->getProxyFactory()
1859
                                        ->getProxy($assoc2['targetDocument'], [$targetClass->identifier => $relatedId]);
1860
                                    $this->registerManaged($other, $relatedId, []);
1861
                                }
1862
                            }
1863
1864 5
                            $prop->setValue($managedCopy, $other);
1865
                        }
1866
                    } else {
1867 10
                        $mergeCol = $prop->getValue($document);
1868
1869 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1870
                            /* Do not merge fields marked lazy that have not
1871
                             * been fetched. Keep the lazy persistent collection
1872
                             * of the managed copy.
1873
                             */
1874 3
                            continue;
1875
                        }
1876
1877 10
                        $managedCol = $prop->getValue($managedCopy);
1878
1879 10
                        if (! $managedCol) {
1880 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1881 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1882 1
                            $prop->setValue($managedCopy, $managedCol);
1883 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1884
                        }
1885
1886
                        /* Note: do not process association's target documents.
1887
                         * They will be handled during the cascade. Initialize
1888
                         * and, if necessary, clear $managedCol for now.
1889
                         */
1890 10
                        if ($assoc2['isCascadeMerge']) {
1891 10
                            $managedCol->initialize();
1892
1893
                            // If $managedCol differs from the merged collection, clear and set dirty
1894 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1895 3
                                $managedCol->unwrap()->clear();
1896 3
                                $managedCol->setDirty(true);
1897
1898 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1899
                                    $this->scheduleForDirtyCheck($managedCopy);
1900
                                }
1901
                            }
1902
                        }
1903
                    }
1904
                }
1905
1906 12
                if (! $class->isChangeTrackingNotify()) {
1907 12
                    continue;
1908
                }
1909
1910
                // Just treat all properties as changed, there is no other choice.
1911
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1912
            }
1913
1914 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1915
                $this->scheduleForDirtyCheck($document);
1916
            }
1917
        }
1918
1919 12
        if ($prevManagedCopy !== null) {
1920 5
            $assocField = $assoc['fieldName'];
1921 5
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1922
1923 5
            if ($assoc['type'] === 'one') {
1924 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1925
            } else {
1926 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1927
1928 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1929 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1930
                }
1931
            }
1932
        }
1933
1934
        // Mark the managed copy visited as well
1935 12
        $visited[spl_object_hash($managedCopy)] = true;
1936
1937 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1938
1939 12
        return $managedCopy;
1940
    }
1941
1942
    /**
1943
     * Detaches a document from the persistence management. It's persistence will
1944
     * no longer be managed by Doctrine.
1945
     *
1946
     * @param object $document The document to detach.
1947
     */
1948 11
    public function detach($document)
1949
    {
1950 11
        $visited = [];
1951 11
        $this->doDetach($document, $visited);
1952 11
    }
1953
1954
    /**
1955
     * Executes a detach operation on the given document.
1956
     *
1957
     * @param object $document
1958
     * @param array  $visited
1959
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1960
     */
1961 16
    private function doDetach($document, array &$visited)
1962
    {
1963 16
        $oid = spl_object_hash($document);
1964 16
        if (isset($visited[$oid])) {
1965 3
            return; // Prevent infinite recursion
1966
        }
1967
1968 16
        $visited[$oid] = $document; // mark visited
1969
1970 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1971 16
            case self::STATE_MANAGED:
1972 16
                $this->removeFromIdentityMap($document);
1973
                unset(
1974 16
                    $this->documentInsertions[$oid],
1975 16
                    $this->documentUpdates[$oid],
1976 16
                    $this->documentDeletions[$oid],
1977 16
                    $this->documentIdentifiers[$oid],
1978 16
                    $this->documentStates[$oid],
1979 16
                    $this->originalDocumentData[$oid],
1980 16
                    $this->parentAssociations[$oid],
1981 16
                    $this->documentUpserts[$oid],
1982 16
                    $this->hasScheduledCollections[$oid],
1983 16
                    $this->embeddedDocumentsRegistry[$oid]
1984
                );
1985 16
                break;
1986 3
            case self::STATE_NEW:
1987 3
            case self::STATE_DETACHED:
1988 3
                return;
1989
        }
1990
1991 16
        $this->cascadeDetach($document, $visited);
1992 16
    }
1993
1994
    /**
1995
     * Refreshes the state of the given document from the database, overwriting
1996
     * any local, unpersisted changes.
1997
     *
1998
     * @param object $document The document to refresh.
1999
     * @throws \InvalidArgumentException If the document is not MANAGED.
2000
     */
2001 23
    public function refresh($document)
2002
    {
2003 23
        $visited = [];
2004 23
        $this->doRefresh($document, $visited);
2005 22
    }
2006
2007
    /**
2008
     * Executes a refresh operation on a document.
2009
     *
2010
     * @param object $document The document to refresh.
2011
     * @param array  $visited  The already visited documents during cascades.
2012
     * @throws \InvalidArgumentException If the document is not MANAGED.
2013
     */
2014 23
    private function doRefresh($document, array &$visited)
2015
    {
2016 23
        $oid = spl_object_hash($document);
2017 23
        if (isset($visited[$oid])) {
2018
            return; // Prevent infinite recursion
2019
        }
2020
2021 23
        $visited[$oid] = $document; // mark visited
2022
2023 23
        $class = $this->dm->getClassMetadata(get_class($document));
2024
2025 23
        if (! $class->isEmbeddedDocument) {
2026 23
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2027 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2028
            }
2029
2030 22
            $this->getDocumentPersister($class->name)->refresh($document);
2031
        }
2032
2033 22
        $this->cascadeRefresh($document, $visited);
2034 22
    }
2035
2036
    /**
2037
     * Cascades a refresh operation to associated documents.
2038
     *
2039
     * @param object $document
2040
     * @param array  $visited
2041
     */
2042 22
    private function cascadeRefresh($document, array &$visited)
2043
    {
2044 22
        $class = $this->dm->getClassMetadata(get_class($document));
2045
2046 22
        $associationMappings = array_filter(
2047 22
            $class->associationMappings,
2048
            function ($assoc) {
2049 17
                return $assoc['isCascadeRefresh'];
2050 22
            }
2051
        );
2052
2053 22
        foreach ($associationMappings as $mapping) {
2054 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2055 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...
2056 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2057
                    // Unwrap so that foreach() does not initialize
2058 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2059
                }
2060 15
                foreach ($relatedDocuments as $relatedDocument) {
2061 15
                    $this->doRefresh($relatedDocument, $visited);
2062
                }
2063 10
            } elseif ($relatedDocuments !== null) {
2064 15
                $this->doRefresh($relatedDocuments, $visited);
2065
            }
2066
        }
2067 22
    }
2068
2069
    /**
2070
     * Cascades a detach operation to associated documents.
2071
     *
2072
     * @param object $document
2073
     * @param array  $visited
2074
     */
2075 16
    private function cascadeDetach($document, array &$visited)
2076
    {
2077 16
        $class = $this->dm->getClassMetadata(get_class($document));
2078 16
        foreach ($class->fieldMappings as $mapping) {
2079 16
            if (! $mapping['isCascadeDetach']) {
2080 16
                continue;
2081
            }
2082 10
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2083 10
            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...
2084 10
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2085
                    // Unwrap so that foreach() does not initialize
2086 7
                    $relatedDocuments = $relatedDocuments->unwrap();
2087
                }
2088 10
                foreach ($relatedDocuments as $relatedDocument) {
2089 10
                    $this->doDetach($relatedDocument, $visited);
2090
                }
2091 10
            } elseif ($relatedDocuments !== null) {
2092 10
                $this->doDetach($relatedDocuments, $visited);
2093
            }
2094
        }
2095 16
    }
2096
    /**
2097
     * Cascades a merge operation to associated documents.
2098
     *
2099
     * @param object $document
2100
     * @param object $managedCopy
2101
     * @param array  $visited
2102
     */
2103 12
    private function cascadeMerge($document, $managedCopy, array &$visited)
2104
    {
2105 12
        $class = $this->dm->getClassMetadata(get_class($document));
2106
2107 12
        $associationMappings = array_filter(
2108 12
            $class->associationMappings,
2109
            function ($assoc) {
2110 12
                return $assoc['isCascadeMerge'];
2111 12
            }
2112
        );
2113
2114 12
        foreach ($associationMappings as $assoc) {
2115 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2116
2117 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...
2118 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2119
                    // Collections are the same, so there is nothing to do
2120 1
                    continue;
2121
                }
2122
2123 8
                foreach ($relatedDocuments as $relatedDocument) {
2124 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2125
                }
2126 6
            } elseif ($relatedDocuments !== null) {
2127 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2128
            }
2129
        }
2130 12
    }
2131
2132
    /**
2133
     * Cascades the save operation to associated documents.
2134
     *
2135
     * @param object $document
2136
     * @param array  $visited
2137
     */
2138 600
    private function cascadePersist($document, array &$visited)
2139
    {
2140 600
        $class = $this->dm->getClassMetadata(get_class($document));
2141
2142 600
        $associationMappings = array_filter(
2143 600
            $class->associationMappings,
2144
            function ($assoc) {
2145 458
                return $assoc['isCascadePersist'];
2146 600
            }
2147
        );
2148
2149 600
        foreach ($associationMappings as $fieldName => $mapping) {
2150 414
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2151
2152 414
            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...
2153 343
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2154 12
                    if ($relatedDocuments->getOwner() !== $document) {
2155 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2156
                    }
2157
                    // Unwrap so that foreach() does not initialize
2158 12
                    $relatedDocuments = $relatedDocuments->unwrap();
2159
                }
2160
2161 343
                $count = 0;
2162 343
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2163 175
                    if (! empty($mapping['embedded'])) {
2164 104
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2165 104
                        if ($knownParent && $knownParent !== $document) {
2166 1
                            $relatedDocument = clone $relatedDocument;
2167 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2168
                        }
2169 104
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2170 104
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2171
                    }
2172 343
                    $this->doPersist($relatedDocument, $visited);
2173
                }
2174 329
            } elseif ($relatedDocuments !== null) {
2175 129
                if (! empty($mapping['embedded'])) {
2176 68
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2177 68
                    if ($knownParent && $knownParent !== $document) {
2178 3
                        $relatedDocuments = clone $relatedDocuments;
2179 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2180
                    }
2181 68
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2182
                }
2183 414
                $this->doPersist($relatedDocuments, $visited);
2184
            }
2185
        }
2186 598
    }
2187
2188
    /**
2189
     * Cascades the delete operation to associated documents.
2190
     *
2191
     * @param object $document
2192
     * @param array  $visited
2193
     */
2194 71
    private function cascadeRemove($document, array &$visited)
2195
    {
2196 71
        $class = $this->dm->getClassMetadata(get_class($document));
2197 71
        foreach ($class->fieldMappings as $mapping) {
2198 71
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2199 70
                continue;
2200
            }
2201 36
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2202 3
                $document->__load();
2203
            }
2204
2205 36
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2206 36
            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...
2207
                // If its a PersistentCollection initialization is intended! No unwrap!
2208 24
                foreach ($relatedDocuments as $relatedDocument) {
2209 24
                    $this->doRemove($relatedDocument, $visited);
2210
                }
2211 25
            } elseif ($relatedDocuments !== null) {
2212 36
                $this->doRemove($relatedDocuments, $visited);
2213
            }
2214
        }
2215 71
    }
2216
2217
    /**
2218
     * Acquire a lock on the given document.
2219
     *
2220
     * @param object $document
2221
     * @param int    $lockMode
2222
     * @param int    $lockVersion
2223
     * @throws LockException
2224
     * @throws \InvalidArgumentException
2225
     */
2226 8
    public function lock($document, $lockMode, $lockVersion = null)
2227
    {
2228 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2229 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2230
        }
2231
2232 7
        $documentName = get_class($document);
2233 7
        $class = $this->dm->getClassMetadata($documentName);
2234
2235 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2236 2
            if (! $class->isVersioned) {
2237 1
                throw LockException::notVersioned($documentName);
2238
            }
2239
2240 1
            if ($lockVersion !== null) {
2241 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2242 1
                if ($documentVersion !== $lockVersion) {
2243 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2244
                }
2245
            }
2246 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2247 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2248
        }
2249 5
    }
2250
2251
    /**
2252
     * Releases a lock on the given document.
2253
     *
2254
     * @param object $document
2255
     * @throws \InvalidArgumentException
2256
     */
2257 1
    public function unlock($document)
2258
    {
2259 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2260
            throw new \InvalidArgumentException('Document is not MANAGED.');
2261
        }
2262 1
        $documentName = get_class($document);
2263 1
        $this->getDocumentPersister($documentName)->unlock($document);
2264 1
    }
2265
2266
    /**
2267
     * Clears the UnitOfWork.
2268
     *
2269
     * @param string|null $documentName if given, only documents of this type will get detached.
2270
     */
2271 373
    public function clear($documentName = null)
2272
    {
2273 373
        if ($documentName === null) {
2274 365
            $this->identityMap =
2275 365
            $this->documentIdentifiers =
2276 365
            $this->originalDocumentData =
2277 365
            $this->documentChangeSets =
2278 365
            $this->documentStates =
2279 365
            $this->scheduledForDirtyCheck =
2280 365
            $this->documentInsertions =
2281 365
            $this->documentUpserts =
2282 365
            $this->documentUpdates =
2283 365
            $this->documentDeletions =
2284 365
            $this->collectionUpdates =
2285 365
            $this->collectionDeletions =
2286 365
            $this->parentAssociations =
2287 365
            $this->embeddedDocumentsRegistry =
2288 365
            $this->orphanRemovals =
2289 365
            $this->hasScheduledCollections = [];
2290
        } else {
2291 8
            $visited = [];
2292 8
            foreach ($this->identityMap as $className => $documents) {
2293 8
                if ($className !== $documentName) {
2294 5
                    continue;
2295
                }
2296
2297 5
                foreach ($documents as $document) {
2298 5
                    $this->doDetach($document, $visited);
2299
                }
2300
            }
2301
        }
2302
2303 373
        if (! $this->evm->hasListeners(Events::onClear)) {
2304 373
            return;
2305
        }
2306
2307
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2308
    }
2309
2310
    /**
2311
     * INTERNAL:
2312
     * Schedules an embedded document for removal. The remove() operation will be
2313
     * invoked on that document at the beginning of the next commit of this
2314
     * UnitOfWork.
2315
     *
2316
     * @ignore
2317
     * @param object $document
2318
     */
2319 47
    public function scheduleOrphanRemoval($document)
2320
    {
2321 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2322 47
    }
2323
2324
    /**
2325
     * INTERNAL:
2326
     * Unschedules an embedded or referenced object for removal.
2327
     *
2328
     * @ignore
2329
     * @param object $document
2330
     */
2331 100
    public function unscheduleOrphanRemoval($document)
2332
    {
2333 100
        $oid = spl_object_hash($document);
2334 100
        unset($this->orphanRemovals[$oid]);
2335 100
    }
2336
2337
    /**
2338
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2339
     *  1) sets owner if it was cloned
2340
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2341
     *  3) NOP if state is OK
2342
     * Returned collection should be used from now on (only important with 2nd point)
2343
     *
2344
     * @param object $document
2345
     * @param string $propName
2346
     * @return PersistentCollectionInterface
2347
     */
2348 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2349
    {
2350 8
        $owner = $coll->getOwner();
2351 8
        if ($owner === null) { // cloned
2352 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2353 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2354 2
            if (! $coll->isInitialized()) {
2355 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2356
            }
2357 2
            $newValue = clone $coll;
2358 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2359 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2360 2
            if ($this->isScheduledForUpdate($document)) {
2361
                // @todo following line should be superfluous once collections are stored in change sets
2362
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2363
            }
2364 2
            return $newValue;
2365
        }
2366 6
        return $coll;
2367
    }
2368
2369
    /**
2370
     * INTERNAL:
2371
     * Schedules a complete collection for removal when this UnitOfWork commits.
2372
     *
2373
     */
2374 35
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2375
    {
2376 35
        $oid = spl_object_hash($coll);
2377 35
        unset($this->collectionUpdates[$oid]);
2378 35
        if (isset($this->collectionDeletions[$oid])) {
2379
            return;
2380
        }
2381
2382 35
        $this->collectionDeletions[$oid] = $coll;
2383 35
        $this->scheduleCollectionOwner($coll);
2384 35
    }
2385
2386
    /**
2387
     * Checks whether a PersistentCollection is scheduled for deletion.
2388
     *
2389
     * @return bool
2390
     */
2391 194
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2392
    {
2393 194
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2394
    }
2395
2396
    /**
2397
     * INTERNAL:
2398
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2399
     *
2400
     */
2401 205
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
2402
    {
2403 205
        $oid = spl_object_hash($coll);
2404 205
        if (! isset($this->collectionDeletions[$oid])) {
2405 205
            return;
2406
        }
2407
2408 5
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2409 5
        unset($this->collectionDeletions[$oid]);
2410 5
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2411 5
    }
2412
2413
    /**
2414
     * INTERNAL:
2415
     * Schedules a collection for update when this UnitOfWork commits.
2416
     *
2417
     */
2418 226
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2419
    {
2420 226
        $mapping = $coll->getMapping();
2421 226
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2422
            /* There is no need to $unset collection if it will be $set later
2423
             * This is NOP if collection is not scheduled for deletion
2424
             */
2425 23
            $this->unscheduleCollectionDeletion($coll);
2426
        }
2427 226
        $oid = spl_object_hash($coll);
2428 226
        if (isset($this->collectionUpdates[$oid])) {
2429 7
            return;
2430
        }
2431
2432 226
        $this->collectionUpdates[$oid] = $coll;
2433 226
        $this->scheduleCollectionOwner($coll);
2434 226
    }
2435
2436
    /**
2437
     * INTERNAL:
2438
     * Unschedules a collection from being updated when this UnitOfWork commits.
2439
     *
2440
     */
2441 205
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
2442
    {
2443 205
        $oid = spl_object_hash($coll);
2444 205
        if (! isset($this->collectionUpdates[$oid])) {
2445 35
            return;
2446
        }
2447
2448 195
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2449 195
        unset($this->collectionUpdates[$oid]);
2450 195
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2451 195
    }
2452
2453
    /**
2454
     * Checks whether a PersistentCollection is scheduled for update.
2455
     *
2456
     * @return bool
2457
     */
2458 114
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2459
    {
2460 114
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2461
    }
2462
2463
    /**
2464
     * INTERNAL:
2465
     * Gets PersistentCollections that have been visited during computing change
2466
     * set of $document
2467
     *
2468
     * @param object $document
2469
     * @return PersistentCollectionInterface[]
2470
     */
2471 553
    public function getVisitedCollections($document)
2472
    {
2473 553
        $oid = spl_object_hash($document);
2474
2475 553
        return $this->visitedCollections[$oid] ?? [];
2476
    }
2477
2478
    /**
2479
     * INTERNAL:
2480
     * Gets PersistentCollections that are scheduled to update and related to $document
2481
     *
2482
     * @param object $document
2483
     * @return array
2484
     */
2485 553
    public function getScheduledCollections($document)
2486
    {
2487 553
        $oid = spl_object_hash($document);
2488
2489 553
        return $this->hasScheduledCollections[$oid] ?? [];
2490
    }
2491
2492
    /**
2493
     * Checks whether the document is related to a PersistentCollection
2494
     * scheduled for update or deletion.
2495
     *
2496
     * @param object $document
2497
     * @return bool
2498
     */
2499 44
    public function hasScheduledCollections($document)
2500
    {
2501 44
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2502
    }
2503
2504
    /**
2505
     * Marks the PersistentCollection's top-level owner as having a relation to
2506
     * a collection scheduled for update or deletion.
2507
     *
2508
     * If the owner is not scheduled for any lifecycle action, it will be
2509
     * scheduled for update to ensure that versioning takes place if necessary.
2510
     *
2511
     * If the collection is nested within atomic collection, it is immediately
2512
     * unscheduled and atomic one is scheduled for update instead. This makes
2513
     * calculating update data way easier.
2514
     *
2515
     */
2516 228
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2517
    {
2518 228
        $document = $this->getOwningDocument($coll->getOwner());
2519 228
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2520
2521 228
        if ($document !== $coll->getOwner()) {
2522 19
            $parent = $coll->getOwner();
2523 19
            $mapping = [];
2524 19
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2525 19
                list($mapping, $parent, ) = $parentAssoc;
2526
            }
2527 19
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2528 3
                $class = $this->dm->getClassMetadata(get_class($document));
2529 3
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2530 3
                $this->scheduleCollectionUpdate($atomicCollection);
2531 3
                $this->unscheduleCollectionDeletion($coll);
2532 3
                $this->unscheduleCollectionUpdate($coll);
2533
            }
2534
        }
2535
2536 228
        if ($this->isDocumentScheduled($document)) {
2537 223
            return;
2538
        }
2539
2540 39
        $this->scheduleForUpdate($document);
2541 39
    }
2542
2543
    /**
2544
     * Get the top-most owning document of a given document
2545
     *
2546
     * If a top-level document is provided, that same document will be returned.
2547
     * For an embedded document, we will walk through parent associations until
2548
     * we find a top-level document.
2549
     *
2550
     * @param object $document
2551
     * @throws \UnexpectedValueException When a top-level document could not be found.
2552
     * @return object
2553
     */
2554 230
    public function getOwningDocument($document)
2555
    {
2556 230
        $class = $this->dm->getClassMetadata(get_class($document));
2557 230
        while ($class->isEmbeddedDocument) {
2558 33
            $parentAssociation = $this->getParentAssociation($document);
2559
2560 33
            if (! $parentAssociation) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentAssociation 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...
2561
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2562
            }
2563
2564 33
            list(, $document, ) = $parentAssociation;
2565 33
            $class = $this->dm->getClassMetadata(get_class($document));
2566
        }
2567
2568 230
        return $document;
2569
    }
2570
2571
    /**
2572
     * Gets the class name for an association (embed or reference) with respect
2573
     * to any discriminator value.
2574
     *
2575
     * @param array      $mapping Field mapping for the association
2576
     * @param array|null $data    Data for the embedded document or reference
2577
     * @return string Class name.
2578
     */
2579 221
    public function getClassNameForAssociation(array $mapping, $data)
2580
    {
2581 221
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2582
2583 221
        $discriminatorValue = null;
2584 221
        if (isset($discriminatorField, $data[$discriminatorField])) {
2585 21
            $discriminatorValue = $data[$discriminatorField];
2586 201
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2587
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2588
        }
2589
2590 221
        if ($discriminatorValue !== null) {
2591 21
            return $mapping['discriminatorMap'][$discriminatorValue]
2592 21
                ?? $discriminatorValue;
2593
        }
2594
2595 201
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2596
2597 201
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2598 15
            $discriminatorValue = $data[$class->discriminatorField];
2599 187
        } elseif ($class->defaultDiscriminatorValue !== null) {
2600 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2601
        }
2602
2603 201
        if ($discriminatorValue !== null) {
2604 16
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2605
        }
2606
2607 186
        return $mapping['targetDocument'];
2608
    }
2609
2610
    /**
2611
     * INTERNAL:
2612
     * Creates a document. Used for reconstitution of documents during hydration.
2613
     *
2614
     * @ignore
2615
     * @param string $className The name of the document class.
2616
     * @param array  $data      The data for the document.
2617
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the document.
2618
     * @param object $document  The document to be hydrated into in case of creation
2619
     * @return object The document instance.
2620
     * @internal Highly performance-sensitive method.
2621
     */
2622 389
    public function getOrCreateDocument($className, $data, &$hints = [], $document = null)
2623
    {
2624 389
        $class = $this->dm->getClassMetadata($className);
2625
2626
        // @TODO figure out how to remove this
2627 389
        $discriminatorValue = null;
2628 389
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2629 17
            $discriminatorValue = $data[$class->discriminatorField];
2630 381
        } elseif (isset($class->defaultDiscriminatorValue)) {
2631 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2632
        }
2633
2634 389
        if ($discriminatorValue !== null) {
2635 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2636
2637 18
            $class = $this->dm->getClassMetadata($className);
2638
2639 18
            unset($data[$class->discriminatorField]);
2640
        }
2641
2642 389
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2643 2
            $document = $class->newInstance();
2644 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2645 2
            return $document;
2646
        }
2647
2648 388
        $isManagedObject = false;
2649 388
        $serializedId = null;
2650 388
        $id = null;
2651 388
        if (! $class->isQueryResultDocument) {
2652 385
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2653 385
            $serializedId = serialize($id);
2654 385
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2655
        }
2656
2657 388
        $oid = null;
2658 388
        if ($isManagedObject) {
2659 96
            $document = $this->identityMap[$class->name][$serializedId];
2660 96
            $oid = spl_object_hash($document);
2661 96
            if ($document instanceof Proxy && ! $document->__isInitialized__) {
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2662 16
                $document->__isInitialized__ = true;
0 ignored issues
show
Bug introduced by
Accessing __isInitialized__ on the interface Doctrine\ODM\MongoDB\Proxy\Proxy suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
2663 16
                $overrideLocalValues = true;
2664 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...
2665 16
                    $document->addPropertyChangedListener($this);
2666
                }
2667
            } else {
2668 86
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2669
            }
2670 96
            if ($overrideLocalValues) {
2671 44
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2672 96
                $this->originalDocumentData[$oid] = $data;
2673
            }
2674
        } else {
2675 345
            if ($document === null) {
2676 345
                $document = $class->newInstance();
2677
            }
2678
2679 345
            if (! $class->isQueryResultDocument) {
2680 341
                $this->registerManaged($document, $id, $data);
2681 341
                $oid = spl_object_hash($document);
2682 341
                $this->documentStates[$oid] = self::STATE_MANAGED;
2683 341
                $this->identityMap[$class->name][$serializedId] = $document;
2684
            }
2685
2686 345
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2687
2688 345
            if (! $class->isQueryResultDocument) {
2689 341
                $this->originalDocumentData[$oid] = $data;
2690
            }
2691
        }
2692
2693 388
        return $document;
2694
    }
2695
2696
    /**
2697
     * Initializes (loads) an uninitialized persistent collection of a document.
2698
     *
2699
     * @param PersistentCollectionInterface $collection The collection to initialize.
2700
     */
2701 164
    public function loadCollection(PersistentCollectionInterface $collection)
2702
    {
2703 164
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2704 164
        $this->lifecycleEventManager->postCollectionLoad($collection);
2705 164
    }
2706
2707
    /**
2708
     * Gets the identity map of the UnitOfWork.
2709
     *
2710
     * @return array
2711
     */
2712
    public function getIdentityMap()
2713
    {
2714
        return $this->identityMap;
2715
    }
2716
2717
    /**
2718
     * Gets the original data of a document. The original data is the data that was
2719
     * present at the time the document was reconstituted from the database.
2720
     *
2721
     * @param object $document
2722
     * @return array
2723
     */
2724 1
    public function getOriginalDocumentData($document)
2725
    {
2726 1
        $oid = spl_object_hash($document);
2727
2728 1
        return $this->originalDocumentData[$oid] ?? [];
2729
    }
2730
2731 61
    public function setOriginalDocumentData($document, array $data)
2732
    {
2733 61
        $oid = spl_object_hash($document);
2734 61
        $this->originalDocumentData[$oid] = $data;
2735 61
        unset($this->documentChangeSets[$oid]);
2736 61
    }
2737
2738
    /**
2739
     * INTERNAL:
2740
     * Sets a property value of the original data array of a document.
2741
     *
2742
     * @ignore
2743
     * @param string $oid
2744
     * @param string $property
2745
     * @param mixed  $value
2746
     */
2747 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2748
    {
2749 3
        $this->originalDocumentData[$oid][$property] = $value;
2750 3
    }
2751
2752
    /**
2753
     * Gets the identifier of a document.
2754
     *
2755
     * @param object $document
2756
     * @return mixed The identifier value
2757
     */
2758 418
    public function getDocumentIdentifier($document)
2759
    {
2760 418
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2761
    }
2762
2763
    /**
2764
     * Checks whether the UnitOfWork has any pending insertions.
2765
     *
2766
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2767
     */
2768
    public function hasPendingInsertions()
2769
    {
2770
        return ! empty($this->documentInsertions);
2771
    }
2772
2773
    /**
2774
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2775
     * number of documents in the identity map.
2776
     *
2777
     * @return int
2778
     */
2779 2
    public function size()
2780
    {
2781 2
        $count = 0;
2782 2
        foreach ($this->identityMap as $documentSet) {
2783 2
            $count += count($documentSet);
2784
        }
2785 2
        return $count;
2786
    }
2787
2788
    /**
2789
     * INTERNAL:
2790
     * Registers a document as managed.
2791
     *
2792
     * TODO: This method assumes that $id is a valid PHP identifier for the
2793
     * document class. If the class expects its database identifier to be an
2794
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2795
     * document identifiers map will become inconsistent with the identity map.
2796
     * In the future, we may want to round-trip $id through a PHP and database
2797
     * conversion and throw an exception if it's inconsistent.
2798
     *
2799
     * @param object $document The document.
2800
     * @param array  $id       The identifier values.
2801
     * @param array  $data     The original document data.
2802
     */
2803 374
    public function registerManaged($document, $id, $data)
2804
    {
2805 374
        $oid = spl_object_hash($document);
2806 374
        $class = $this->dm->getClassMetadata(get_class($document));
2807
2808 374
        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...
2809 95
            $this->documentIdentifiers[$oid] = $oid;
2810
        } else {
2811 368
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2812
        }
2813
2814 374
        $this->documentStates[$oid] = self::STATE_MANAGED;
2815 374
        $this->originalDocumentData[$oid] = $data;
2816 374
        $this->addToIdentityMap($document);
2817 374
    }
2818
2819
    /**
2820
     * INTERNAL:
2821
     * Clears the property changeset of the document with the given OID.
2822
     *
2823
     * @param string $oid The document's OID.
2824
     */
2825
    public function clearDocumentChangeSet($oid)
2826
    {
2827
        $this->documentChangeSets[$oid] = [];
2828
    }
2829
2830
    /* PropertyChangedListener implementation */
2831
2832
    /**
2833
     * Notifies this UnitOfWork of a property change in a document.
2834
     *
2835
     * @param object $document     The document that owns the property.
2836
     * @param string $propertyName The name of the property that changed.
2837
     * @param mixed  $oldValue     The old value of the property.
2838
     * @param mixed  $newValue     The new value of the property.
2839
     */
2840 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2841
    {
2842 2
        $oid = spl_object_hash($document);
2843 2
        $class = $this->dm->getClassMetadata(get_class($document));
2844
2845 2
        if (! isset($class->fieldMappings[$propertyName])) {
2846 1
            return; // ignore non-persistent fields
2847
        }
2848
2849
        // Update changeset and mark document for synchronization
2850 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2851 2
        if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2852
            return;
2853
        }
2854
2855 2
        $this->scheduleForDirtyCheck($document);
2856 2
    }
2857
2858
    /**
2859
     * Gets the currently scheduled document insertions in this UnitOfWork.
2860
     *
2861
     * @return array
2862
     */
2863 2
    public function getScheduledDocumentInsertions()
2864
    {
2865 2
        return $this->documentInsertions;
2866
    }
2867
2868
    /**
2869
     * Gets the currently scheduled document upserts in this UnitOfWork.
2870
     *
2871
     * @return array
2872
     */
2873 1
    public function getScheduledDocumentUpserts()
2874
    {
2875 1
        return $this->documentUpserts;
2876
    }
2877
2878
    /**
2879
     * Gets the currently scheduled document updates in this UnitOfWork.
2880
     *
2881
     * @return array
2882
     */
2883 1
    public function getScheduledDocumentUpdates()
2884
    {
2885 1
        return $this->documentUpdates;
2886
    }
2887
2888
    /**
2889
     * Gets the currently scheduled document deletions in this UnitOfWork.
2890
     *
2891
     * @return array
2892
     */
2893
    public function getScheduledDocumentDeletions()
2894
    {
2895
        return $this->documentDeletions;
2896
    }
2897
2898
    /**
2899
     * Get the currently scheduled complete collection deletions
2900
     *
2901
     * @return array
2902
     */
2903
    public function getScheduledCollectionDeletions()
2904
    {
2905
        return $this->collectionDeletions;
2906
    }
2907
2908
    /**
2909
     * Gets the currently scheduled collection inserts, updates and deletes.
2910
     *
2911
     * @return array
2912
     */
2913
    public function getScheduledCollectionUpdates()
2914
    {
2915
        return $this->collectionUpdates;
2916
    }
2917
2918
    /**
2919
     * Helper method to initialize a lazy loading proxy or persistent collection.
2920
     *
2921
     * @param object $obj
2922
     */
2923
    public function initializeObject($obj)
2924
    {
2925
        if ($obj instanceof Proxy) {
2926
            $obj->__load();
2927
        } elseif ($obj instanceof PersistentCollectionInterface) {
2928
            $obj->initialize();
2929
        }
2930
    }
2931
2932
    private function objToStr($obj)
2933
    {
2934
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2935
    }
2936
}
2937