Completed
Pull Request — master (#1787)
by Stefano
15:28
created

UnitOfWork::getDocumentChangeSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
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\PersistentCollection\PersistentCollectionInterface;
15
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
16
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
17
use Doctrine\ODM\MongoDB\Proxy\Proxy;
18
use Doctrine\ODM\MongoDB\Query\Query;
19
use Doctrine\ODM\MongoDB\Types\DateType;
20
use Doctrine\ODM\MongoDB\Types\Type;
21
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
22
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
23
use MongoDB\BSON\UTCDateTime;
24
use function array_filter;
25
use function count;
26
use function get_class;
27
use function in_array;
28
use function is_array;
29
use function is_object;
30
use function method_exists;
31
use function preg_match;
32
use function serialize;
33
use function spl_object_hash;
34
use function sprintf;
35
36
/**
37
 * The UnitOfWork is responsible for tracking changes to objects during an
38
 * "object-level" transaction and for writing out changes to the database
39
 * in the correct order.
40
 *
41
 */
42
class UnitOfWork implements PropertyChangedListener
43
{
44
    /**
45
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
46
     */
47
    public const STATE_MANAGED = 1;
48
49
    /**
50
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
51
     * and is not (yet) managed by a DocumentManager.
52
     */
53
    public const STATE_NEW = 2;
54
55
    /**
56
     * A detached document is an instance with a persistent identity that is not
57
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
58
     */
59
    public const STATE_DETACHED = 3;
60
61
    /**
62
     * A removed document instance is an instance with a persistent identity,
63
     * associated with a DocumentManager, whose persistent state has been
64
     * deleted (or is scheduled for deletion).
65
     */
66
    public const STATE_REMOVED = 4;
67
68
    /**
69
     * The identity map holds references to all managed documents.
70
     *
71
     * Documents are grouped by their class name, and then indexed by the
72
     * serialized string of their database identifier field or, if the class
73
     * has no identifier, the SPL object hash. Serializing the identifier allows
74
     * differentiation of values that may be equal (via type juggling) but not
75
     * identical.
76
     *
77
     * Since all classes in a hierarchy must share the same identifier set,
78
     * we always take the root class name of the hierarchy.
79
     *
80
     * @var array
81
     */
82
    private $identityMap = [];
83
84
    /**
85
     * Map of all identifiers of managed documents.
86
     * Keys are object ids (spl_object_hash).
87
     *
88
     * @var array
89
     */
90
    private $documentIdentifiers = [];
91
92
    /**
93
     * Map of the original document data of managed documents.
94
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
95
     * at commit time.
96
     *
97
     * @var array
98
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
99
     *           A value will only really be copied if the value in the document is modified
100
     *           by the user.
101
     */
102
    private $originalDocumentData = [];
103
104
    /**
105
     * Map of document changes. Keys are object ids (spl_object_hash).
106
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
107
     *
108
     * @var array
109
     */
110
    private $documentChangeSets = [];
111
112
    /**
113
     * The (cached) states of any known documents.
114
     * Keys are object ids (spl_object_hash).
115
     *
116
     * @var array
117
     */
118
    private $documentStates = [];
119
120
    /**
121
     * Map of documents that are scheduled for dirty checking at commit time.
122
     *
123
     * Documents are grouped by their class name, and then indexed by their SPL
124
     * object hash. This is only used for documents with a change tracking
125
     * policy of DEFERRED_EXPLICIT.
126
     *
127
     * @var array
128
     * @todo rename: scheduledForSynchronization
129
     */
130
    private $scheduledForDirtyCheck = [];
131
132
    /**
133
     * A list of all pending document insertions.
134
     *
135
     * @var array
136
     */
137
    private $documentInsertions = [];
138
139
    /**
140
     * A list of all pending document updates.
141
     *
142
     * @var array
143
     */
144
    private $documentUpdates = [];
145
146
    /**
147
     * A list of all pending document upserts.
148
     *
149
     * @var array
150
     */
151
    private $documentUpserts = [];
152
153
    /**
154
     * A list of all pending document deletions.
155
     *
156
     * @var array
157
     */
158
    private $documentDeletions = [];
159
160
    /**
161
     * All pending collection deletions.
162
     *
163
     * @var array
164
     */
165
    private $collectionDeletions = [];
166
167
    /**
168
     * All pending collection updates.
169
     *
170
     * @var array
171
     */
172
    private $collectionUpdates = [];
173
174
    /**
175
     * A list of documents related to collections scheduled for update or deletion
176
     *
177
     * @var array
178
     */
179
    private $hasScheduledCollections = [];
180
181
    /**
182
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
183
     * At the end of the UnitOfWork all these collections will make new snapshots
184
     * of their data.
185
     *
186
     * @var array
187
     */
188
    private $visitedCollections = [];
189
190
    /**
191
     * The DocumentManager that "owns" this UnitOfWork instance.
192
     *
193
     * @var DocumentManager
194
     */
195
    private $dm;
196
197
    /**
198
     * The EventManager used for dispatching events.
199
     *
200
     * @var EventManager
201
     */
202
    private $evm;
203
204
    /**
205
     * Additional documents that are scheduled for removal.
206
     *
207
     * @var array
208
     */
209
    private $orphanRemovals = [];
210
211
    /**
212
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
213
     *
214
     * @var HydratorFactory
215
     */
216
    private $hydratorFactory;
217
218
    /**
219
     * The document persister instances used to persist document instances.
220
     *
221
     * @var array
222
     */
223
    private $persisters = [];
224
225
    /**
226
     * The collection persister instance used to persist changes to collections.
227
     *
228
     * @var Persisters\CollectionPersister
229
     */
230
    private $collectionPersister;
231
232
    /**
233
     * The persistence builder instance used in DocumentPersisters.
234
     *
235
     * @var PersistenceBuilder
236
     */
237
    private $persistenceBuilder;
238
239
    /**
240
     * Array of parent associations between embedded documents.
241
     *
242
     * @var array
243
     */
244
    private $parentAssociations = [];
245
246
    /** @var LifecycleEventManager */
247
    private $lifecycleEventManager;
248
249
    /**
250
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
251
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
252
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
253
     *
254
     * @var array
255
     */
256
    private $embeddedDocumentsRegistry = [];
257
258
    /** @var int */
259
    private $commitsInProgress = 0;
260
261
    /**
262
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
263
     *
264
     */
265 1580
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
266
    {
267 1580
        $this->dm = $dm;
268 1580
        $this->evm = $evm;
269 1580
        $this->hydratorFactory = $hydratorFactory;
270 1580
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
271 1580
    }
272
273
    /**
274
     * Factory for returning new PersistenceBuilder instances used for preparing data into
275
     * queries for insert persistence.
276
     *
277
     * @return PersistenceBuilder $pb
278
     */
279 1076
    public function getPersistenceBuilder()
280
    {
281 1076
        if (! $this->persistenceBuilder) {
282 1076
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
283
        }
284 1076
        return $this->persistenceBuilder;
285
    }
286
287
    /**
288
     * Sets the parent association for a given embedded document.
289
     *
290
     * @param object $document
291
     * @param array  $mapping
292
     * @param object $parent
293
     * @param string $propertyPath
294
     */
295 177
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
296
    {
297 177
        $oid = spl_object_hash($document);
298 177
        $this->embeddedDocumentsRegistry[$oid] = $document;
299 177
        $this->parentAssociations[$oid] = [$mapping, $parent, $propertyPath];
300 177
    }
301
302
    /**
303
     * Gets the parent association for a given embedded document.
304
     *
305
     *     <code>
306
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
307
     *     </code>
308
     *
309
     * @param object $document
310
     * @return array $association
311
     */
312 204
    public function getParentAssociation($document)
313
    {
314 204
        $oid = spl_object_hash($document);
315
316 204
        return $this->parentAssociations[$oid] ?? null;
317
    }
318
319
    /**
320
     * Get the document persister instance for the given document name
321
     *
322
     * @param string $documentName
323
     * @return Persisters\DocumentPersister
324
     */
325 1074
    public function getDocumentPersister($documentName)
326
    {
327 1074
        if (! isset($this->persisters[$documentName])) {
328 1061
            $class = $this->dm->getClassMetadata($documentName);
329 1061
            $pb = $this->getPersistenceBuilder();
330 1061
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
331
        }
332 1074
        return $this->persisters[$documentName];
333
    }
334
335
    /**
336
     * Get the collection persister instance.
337
     *
338
     * @return CollectionPersister
339
     */
340 1074
    public function getCollectionPersister()
341
    {
342 1074
        if (! isset($this->collectionPersister)) {
343 1074
            $pb = $this->getPersistenceBuilder();
344 1074
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
345
        }
346 1074
        return $this->collectionPersister;
347
    }
348
349
    /**
350
     * Set the document persister instance to use for the given document name
351
     *
352
     * @param string $documentName
353
     */
354 13
    public function setDocumentPersister($documentName, Persisters\DocumentPersister $persister)
355
    {
356 13
        $this->persisters[$documentName] = $persister;
357 13
    }
358
359
    /**
360
     * Commits the UnitOfWork, executing all operations that have been postponed
361
     * up to this point. The state of all managed documents will be synchronized with
362
     * the database.
363
     *
364
     * The operations are executed in the following order:
365
     *
366
     * 1) All document insertions
367
     * 2) All document updates
368
     * 3) All document deletions
369
     *
370
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
371
     */
372 566
    public function commit(array $options = [])
373
    {
374
        // Raise preFlush
375 566
        if ($this->evm->hasListeners(Events::preFlush)) {
376
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
377
        }
378
379
        // Compute changes done since last commit.
380 566
        $this->computeChangeSets();
381
382 565
        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...
383 240
            $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...
384 199
            $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...
385 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...
386 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...
387 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...
388 565
            $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...
389
        ) {
390 22
            return; // Nothing to do.
391
        }
392
393 562
        $this->commitsInProgress++;
394 562
        if ($this->commitsInProgress > 1) {
395
            throw MongoDBException::commitInProgress();
396
        }
397
        try {
398 562
            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...
399 44
                foreach ($this->orphanRemovals as $removal) {
400 44
                    $this->remove($removal);
401
                }
402
            }
403
404
            // Raise onFlush
405 562
            if ($this->evm->hasListeners(Events::onFlush)) {
406 4
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
407
            }
408
409 561
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
410 84
                list($class, $documents) = $classAndDocuments;
411 84
                $this->executeUpserts($class, $documents, $options);
412
            }
413
414 561
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
415 488
                list($class, $documents) = $classAndDocuments;
416 488
                $this->executeInserts($class, $documents, $options);
417
            }
418
419 560
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
420 205
                list($class, $documents) = $classAndDocuments;
421 205
                $this->executeUpdates($class, $documents, $options);
422
            }
423
424 560
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
425 64
                list($class, $documents) = $classAndDocuments;
426 64
                $this->executeDeletions($class, $documents, $options);
427
            }
428
429
            // Raise postFlush
430 560
            if ($this->evm->hasListeners(Events::postFlush)) {
431
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
432
            }
433
434
            // Clear up
435 560
            $this->documentInsertions =
436 560
            $this->documentUpserts =
437 560
            $this->documentUpdates =
438 560
            $this->documentDeletions =
439 560
            $this->documentChangeSets =
440 560
            $this->collectionUpdates =
441 560
            $this->collectionDeletions =
442 560
            $this->visitedCollections =
443 560
            $this->scheduledForDirtyCheck =
444 560
            $this->orphanRemovals =
445 560
            $this->hasScheduledCollections = [];
446 560
        } finally {
447 562
            $this->commitsInProgress--;
448
        }
449 560
    }
450
451
    /**
452
     * Groups a list of scheduled documents by their class.
453
     *
454
     * @param array $documents       Scheduled documents (e.g. $this->documentInsertions)
455
     * @param bool  $includeEmbedded
456
     * @return array Tuples of ClassMetadata and a corresponding array of objects
457
     */
458 561
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
459
    {
460 561
        if (empty($documents)) {
461 561
            return [];
462
        }
463 560
        $divided = [];
464 560
        $embeds = [];
465 560
        foreach ($documents as $oid => $d) {
466 560
            $className = get_class($d);
467 560
            if (isset($embeds[$className])) {
468 69
                continue;
469
            }
470 560
            if (isset($divided[$className])) {
471 155
                $divided[$className][1][$oid] = $d;
472 155
                continue;
473
            }
474 560
            $class = $this->dm->getClassMetadata($className);
475 560
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
476 154
                $embeds[$className] = true;
477 154
                continue;
478
            }
479 560
            if (empty($divided[$class->name])) {
480 560
                $divided[$class->name] = [$class, [$oid => $d]];
481
            } else {
482 560
                $divided[$class->name][1][$oid] = $d;
483
            }
484
        }
485 560
        return $divided;
486
    }
487
488
    /**
489
     * Compute changesets of all documents scheduled for insertion.
490
     *
491
     * Embedded documents will not be processed.
492
     */
493 569
    private function computeScheduleInsertsChangeSets()
494
    {
495 569
        foreach ($this->documentInsertions as $document) {
496 499
            $class = $this->dm->getClassMetadata(get_class($document));
497 499
            if ($class->isEmbeddedDocument) {
498 138
                continue;
499
            }
500
501 494
            $this->computeChangeSet($class, $document);
502
        }
503 568
    }
504
505
    /**
506
     * Compute changesets of all documents scheduled for upsert.
507
     *
508
     * Embedded documents will not be processed.
509
     */
510 568
    private function computeScheduleUpsertsChangeSets()
511
    {
512 568
        foreach ($this->documentUpserts as $document) {
513 83
            $class = $this->dm->getClassMetadata(get_class($document));
514 83
            if ($class->isEmbeddedDocument) {
515
                continue;
516
            }
517
518 83
            $this->computeChangeSet($class, $document);
519
        }
520 568
    }
521
522
    /**
523
     * Gets the changeset for a document.
524
     *
525
     * @param object $document
526
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
527
     */
528 563
    public function getDocumentChangeSet($document)
529
    {
530 563
        $oid = spl_object_hash($document);
531
532 563
        return $this->documentChangeSets[$oid] ?? [];
533
    }
534
535
    /**
536
     * INTERNAL:
537
     * Sets the changeset for a document.
538
     *
539
     * @param object $document
540
     * @param array  $changeset
541
     */
542 1
    public function setDocumentChangeSet($document, $changeset)
543
    {
544 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
545 1
    }
546
547
    /**
548
     * Get a documents actual data, flattening all the objects to arrays.
549
     *
550
     * @param object $document
551
     * @return array
552
     */
553 570
    public function getDocumentActualData($document)
554
    {
555 570
        $class = $this->dm->getClassMetadata(get_class($document));
556 570
        $actualData = [];
557 570
        foreach ($class->reflFields as $name => $refProp) {
558 570
            $mapping = $class->fieldMappings[$name];
559
            // skip not saved fields
560 570
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
561 27
                continue;
562
            }
563 570
            $value = $refProp->getValue($document);
564 570
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
565 570
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
566
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
567 366
                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...
568 140
                    $value = new ArrayCollection($value);
569
                }
570
571
                // Inject PersistentCollection
572 366
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
573 366
                $coll->setOwner($document, $mapping);
574 366
                $coll->setDirty(! $value->isEmpty());
575 366
                $class->reflFields[$name]->setValue($document, $coll);
576 366
                $actualData[$name] = $coll;
577
            } else {
578 570
                $actualData[$name] = $value;
579
            }
580
        }
581 570
        return $actualData;
582
    }
583
584
    /**
585
     * Computes the changes that happened to a single document.
586
     *
587
     * Modifies/populates the following properties:
588
     *
589
     * {@link originalDocumentData}
590
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
591
     * then it was not fetched from the database and therefore we have no original
592
     * document data yet. All of the current document data is stored as the original document data.
593
     *
594
     * {@link documentChangeSets}
595
     * The changes detected on all properties of the document are stored there.
596
     * A change is a tuple array where the first entry is the old value and the second
597
     * entry is the new value of the property. Changesets are used by persisters
598
     * to INSERT/UPDATE the persistent document state.
599
     *
600
     * {@link documentUpdates}
601
     * If the document is already fully MANAGED (has been fetched from the database before)
602
     * and any changes to its properties are detected, then a reference to the document is stored
603
     * there to mark it for an update.
604
     *
605
     * @param ClassMetadata $class    The class descriptor of the document.
606
     * @param object        $document The document for which to compute the changes.
607
     */
608 566
    public function computeChangeSet(ClassMetadata $class, $document)
609
    {
610 566
        if (! $class->isInheritanceTypeNone()) {
611 174
            $class = $this->dm->getClassMetadata(get_class($document));
612
        }
613
614
        // Fire PreFlush lifecycle callbacks
615 566
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
616 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
617
        }
618
619 566
        $this->computeOrRecomputeChangeSet($class, $document);
620 565
    }
621
622
    /**
623
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
624
     *
625
     * @param object $document
626
     * @param bool   $recompute
627
     */
628 566
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
629
    {
630 566
        $oid = spl_object_hash($document);
631 566
        $actualData = $this->getDocumentActualData($document);
632 566
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
633 566
        if ($isNewDocument) {
634
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
635
            // These result in an INSERT.
636 566
            $this->originalDocumentData[$oid] = $actualData;
637 566
            $changeSet = [];
638 566
            foreach ($actualData as $propName => $actualValue) {
639
                /* At this PersistentCollection shouldn't be here, probably it
640
                 * was cloned and its ownership must be fixed
641
                 */
642 566
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
643
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
644
                    $actualValue = $actualData[$propName];
645
                }
646
                // ignore inverse side of reference relationship
647 566
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
648 179
                    continue;
649
                }
650 566
                $changeSet[$propName] = [null, $actualValue];
651
            }
652 566
            $this->documentChangeSets[$oid] = $changeSet;
653
        } else {
654 259
            if ($class->isReadOnly) {
655 2
                return;
656
            }
657
            // Document is "fully" MANAGED: it was already fully persisted before
658
            // and we have a copy of the original data
659 257
            $originalData = $this->originalDocumentData[$oid];
660 257
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
661 257
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
662 2
                $changeSet = $this->documentChangeSets[$oid];
663
            } else {
664 257
                $changeSet = [];
665
            }
666
667 257
            foreach ($actualData as $propName => $actualValue) {
668
                // skip not saved fields
669 257
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
670
                    continue;
671
                }
672
673 257
                $orgValue = $originalData[$propName] ?? null;
674
675
                // skip if value has not changed
676 257
                if ($orgValue === $actualValue) {
677 256
                    if (! $actualValue instanceof PersistentCollectionInterface) {
678 256
                        continue;
679
                    }
680
681 177
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
682
                        // consider dirty collections as changed as well
683 153
                        continue;
684
                    }
685
                }
686
687
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
688 221
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
689 13
                    if ($orgValue !== null) {
690 8
                        $this->scheduleOrphanRemoval($orgValue);
691
                    }
692 13
                    $changeSet[$propName] = [$orgValue, $actualValue];
693 13
                    continue;
694
                }
695
696
                // if owning side of reference-one relationship
697 215
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
698 12
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
699 1
                        $this->scheduleOrphanRemoval($orgValue);
700
                    }
701
702 12
                    $changeSet[$propName] = [$orgValue, $actualValue];
703 12
                    continue;
704
                }
705
706 209
                if ($isChangeTrackingNotify) {
707 3
                    continue;
708
                }
709
710
                // ignore inverse side of reference relationship
711 207
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
712 6
                    continue;
713
                }
714
715
                // Persistent collection was exchanged with the "originally"
716
                // created one. This can only mean it was cloned and replaced
717
                // on another document.
718 205
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
719 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
720
                }
721
722
                // if embed-many or reference-many relationship
723 205
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
724 101
                    $changeSet[$propName] = [$orgValue, $actualValue];
725
                    /* If original collection was exchanged with a non-empty value
726
                     * and $set will be issued, there is no need to $unset it first
727
                     */
728 101
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
729 19
                        continue;
730
                    }
731 88
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
732 15
                        $this->scheduleCollectionDeletion($orgValue);
733
                    }
734 88
                    continue;
735
                }
736
737
                // skip equivalent date values
738 133
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
739
                    /** @var DateType $dateType */
740 37
                    $dateType = Type::getType('date');
741 37
                    $dbOrgValue = $dateType->convertToDatabaseValue($orgValue);
742 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
743
744 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...
745 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...
746
747 37
                    if ($orgTimestamp === $actualTimestamp) {
748 30
                        continue;
749
                    }
750
                }
751
752
                // regular field
753 116
                $changeSet[$propName] = [$orgValue, $actualValue];
754
            }
755 257
            if ($changeSet) {
756 210
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
757 16
                    ? $changeSet + $this->documentChangeSets[$oid]
758 208
                    : $changeSet;
759
760 210
                $this->originalDocumentData[$oid] = $actualData;
761 210
                $this->scheduleForUpdate($document);
762
            }
763
        }
764
765
        // Look for changes in associations of the document
766 566
        $associationMappings = array_filter(
767 566
            $class->associationMappings,
768
            function ($assoc) {
769 429
                return empty($assoc['notSaved']);
770 566
            }
771
        );
772
773 566
        foreach ($associationMappings as $mapping) {
774 429
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
775
776 429
            if ($value === null) {
777 292
                continue;
778
            }
779
780 418
            $this->computeAssociationChanges($document, $mapping, $value);
781
782 417
            if (isset($mapping['reference'])) {
783 314
                continue;
784
            }
785
786 321
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
787
788 321
            foreach ($values as $obj) {
789 158
                $oid2 = spl_object_hash($obj);
790
791 158
                if (isset($this->documentChangeSets[$oid2])) {
792 156
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
793
                        // instance of $value is the same as it was previously otherwise there would be
794
                        // change set already in place
795 34
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
796
                    }
797
798 156
                    if (! $isNewDocument) {
799 65
                        $this->scheduleForUpdate($document);
800
                    }
801
802 321
                    break;
803
                }
804
            }
805
        }
806 565
    }
807
808
    /**
809
     * Computes all the changes that have been done to documents and collections
810
     * since the last commit and stores these changes in the _documentChangeSet map
811
     * temporarily for access by the persisters, until the UoW commit is finished.
812
     */
813 569
    public function computeChangeSets()
814
    {
815 569
        $this->computeScheduleInsertsChangeSets();
816 568
        $this->computeScheduleUpsertsChangeSets();
817
818
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
819 568
        foreach ($this->identityMap as $className => $documents) {
820 568
            $class = $this->dm->getClassMetadata($className);
821 568
            if ($class->isEmbeddedDocument) {
822
                /* we do not want to compute changes to embedded documents up front
823
                 * in case embedded document was replaced and its changeset
824
                 * would corrupt data. Embedded documents' change set will
825
                 * be calculated by reachability from owning document.
826
                 */
827 150
                continue;
828
            }
829
830
            // If change tracking is explicit or happens through notification, then only compute
831
            // changes on document of that type that are explicitly marked for synchronization.
832
            switch (true) {
833 568
                case ($class->isChangeTrackingDeferredImplicit()):
834 567
                    $documentsToProcess = $documents;
835 567
                    break;
836
837 4
                case (isset($this->scheduledForDirtyCheck[$className])):
838 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
839 3
                    break;
840
841
                default:
842 4
                    $documentsToProcess = [];
843
            }
844
845 568
            foreach ($documentsToProcess as $document) {
846
                // Ignore uninitialized proxy objects
847 564
                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...
848 9
                    continue;
849
                }
850
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
851 564
                $oid = spl_object_hash($document);
852 564
                if (isset($this->documentInsertions[$oid])
853 299
                    || isset($this->documentUpserts[$oid])
854 255
                    || isset($this->documentDeletions[$oid])
855 564
                    || ! isset($this->documentStates[$oid])
856
                ) {
857 564
                    continue;
858
                }
859
860 568
                $this->computeChangeSet($class, $document);
861
            }
862
        }
863 568
    }
864
865
    /**
866
     * Computes the changes of an association.
867
     *
868
     * @param object $parentDocument
869
     * @param array  $assoc
870
     * @param mixed  $value          The value of the association.
871
     * @throws \InvalidArgumentException
872
     */
873 418
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
874
    {
875 418
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
876 418
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
877 418
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
878
879 418
        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...
880 7
            return;
881
        }
882
883 417
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
884 228
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
885 224
                $this->scheduleCollectionUpdate($value);
886
            }
887 228
            $topmostOwner = $this->getOwningDocument($value->getOwner());
888 228
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
889 228
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
890 122
                $value->initialize();
891 122
                foreach ($value->getDeletedDocuments() as $orphan) {
892 20
                    $this->scheduleOrphanRemoval($orphan);
893
                }
894
            }
895
        }
896
897
        // Look through the documents, and in any of their associations,
898
        // for transient (new) documents, recursively. ("Persistence by reachability")
899
        // Unwrap. Uninitialized collections will simply be empty.
900 417
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap();
901
902 417
        $count = 0;
903 417
        foreach ($unwrappedValue as $key => $entry) {
904 335
            if (! is_object($entry)) {
905 1
                throw new \InvalidArgumentException(
906 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
907
                );
908
            }
909
910 334
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
911
912 334
            $state = $this->getDocumentState($entry, self::STATE_NEW);
913
914
            // Handle "set" strategy for multi-level hierarchy
915 334
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
916 334
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
917
918 334
            $count++;
919
920
            switch ($state) {
921 334
                case self::STATE_NEW:
922 53
                    if (! $assoc['isCascadePersist']) {
923
                        throw new \InvalidArgumentException('A new document was found through a relationship that was not'
924
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
925
                            . ' Explicitly persist the new document or configure cascading persist operations'
926
                            . ' on the relationship.');
927
                    }
928
929 53
                    $this->persistNew($targetClass, $entry);
930 53
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
931 53
                    $this->computeChangeSet($targetClass, $entry);
932 53
                    break;
933
934 330
                case self::STATE_MANAGED:
935 330
                    if ($targetClass->isEmbeddedDocument) {
936 150
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
937 150
                        if ($knownParent && $knownParent !== $parentDocument) {
938 6
                            $entry = clone $entry;
939 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
940 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
941 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
942 3
                                $poid = spl_object_hash($parentDocument);
943 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
944 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
945
                                }
946
                            } else {
947
                                // must use unwrapped value to not trigger orphan removal
948 4
                                $unwrappedValue[$key] = $entry;
949
                            }
950 6
                            $this->persistNew($targetClass, $entry);
951
                        }
952 150
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
953 150
                        $this->computeChangeSet($targetClass, $entry);
954
                    }
955 330
                    break;
956
957 1
                case self::STATE_REMOVED:
958
                    // Consume the $value as array (it's either an array or an ArrayAccess)
959
                    // and remove the element from Collection.
960 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
961
                        unset($value[$key]);
962
                    }
963 1
                    break;
964
965
                case self::STATE_DETACHED:
966
                    // Can actually not happen right now as we assume STATE_NEW,
967
                    // so the exception will be raised from the DBAL layer (constraint violation).
968
                    throw new \InvalidArgumentException('A detached document was found through a '
969
                        . 'relationship during cascading a persist operation.');
970
971 334
                default:
972
                    // MANAGED associated documents are already taken into account
973
                    // during changeset calculation anyway, since they are in the identity map.
974
            }
975
        }
976 416
    }
977
978
    /**
979
     * INTERNAL:
980
     * Computes the changeset of an individual document, independently of the
981
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
982
     *
983
     * The passed document must be a managed document. If the document already has a change set
984
     * because this method is invoked during a commit cycle then the change sets are added.
985
     * whereby changes detected in this method prevail.
986
     *
987
     * @ignore
988
     * @param ClassMetadata $class    The class descriptor of the document.
989
     * @param object        $document The document for which to (re)calculate the change set.
990
     * @throws \InvalidArgumentException If the passed document is not MANAGED.
991
     */
992 17
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, $document)
993
    {
994
        // Ignore uninitialized proxy objects
995 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...
996 1
            return;
997
        }
998
999 16
        $oid = spl_object_hash($document);
1000
1001 16
        if (! isset($this->documentStates[$oid]) || $this->documentStates[$oid] !== self::STATE_MANAGED) {
1002
            throw new \InvalidArgumentException('Document must be managed.');
1003
        }
1004
1005 16
        if (! $class->isInheritanceTypeNone()) {
1006 1
            $class = $this->dm->getClassMetadata(get_class($document));
1007
        }
1008
1009 16
        $this->computeOrRecomputeChangeSet($class, $document, true);
1010 16
    }
1011
1012
    /**
1013
     * @param object $document
1014
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1015
     */
1016 596
    private function persistNew(ClassMetadata $class, $document)
1017
    {
1018 596
        $this->lifecycleEventManager->prePersist($class, $document);
1019 596
        $oid = spl_object_hash($document);
1020 596
        $upsert = false;
1021 596
        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...
1022 596
            $idValue = $class->getIdentifierValue($document);
1023 596
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1024
1025 596
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1026 3
                throw new \InvalidArgumentException(sprintf(
1027 3
                    '%s uses NONE identifier generation strategy but no identifier was provided when persisting.',
1028 3
                    get_class($document)
1029
                ));
1030
            }
1031
1032 595
            if ($class->generatorType === ClassMetadata::GENERATOR_TYPE_AUTO && $idValue !== null && ! preg_match('#^[0-9a-f]{24}$#', (string) $idValue)) {
1033 1
                throw new \InvalidArgumentException(sprintf(
1034 1
                    '%s uses AUTO identifier generation strategy but provided identifier is not a valid ObjectId.',
1035 1
                    get_class($document)
1036
                ));
1037
            }
1038
1039 594
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1040 517
                $idValue = $class->idGenerator->generate($this->dm, $document);
1041 517
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1042 517
                $class->setIdentifierValue($document, $idValue);
1043
            }
1044
1045 594
            $this->documentIdentifiers[$oid] = $idValue;
1046
        } else {
1047
            // this is for embedded documents without identifiers
1048 130
            $this->documentIdentifiers[$oid] = $oid;
1049
        }
1050
1051 594
        $this->documentStates[$oid] = self::STATE_MANAGED;
1052
1053 594
        if ($upsert) {
1054 87
            $this->scheduleForUpsert($class, $document);
1055
        } else {
1056 525
            $this->scheduleForInsert($class, $document);
1057
        }
1058 594
    }
1059
1060
    /**
1061
     * Executes all document insertions for documents of the specified type.
1062
     *
1063
     * @param array $documents Array of documents to insert
1064
     * @param array $options   Array of options to be used with batchInsert()
1065
     */
1066 488
    private function executeInserts(ClassMetadata $class, array $documents, array $options = [])
1067
    {
1068 488
        $persister = $this->getDocumentPersister($class->name);
1069
1070 488
        foreach ($documents as $oid => $document) {
1071 488
            $persister->addInsert($document);
1072 488
            unset($this->documentInsertions[$oid]);
1073
        }
1074
1075 488
        $persister->executeInserts($options);
1076
1077 487
        foreach ($documents as $document) {
1078 487
            $this->lifecycleEventManager->postPersist($class, $document);
1079
        }
1080 487
    }
1081
1082
    /**
1083
     * Executes all document upserts for documents of the specified type.
1084
     *
1085
     * @param array $documents Array of documents to upsert
1086
     * @param array $options   Array of options to be used with batchInsert()
1087
     */
1088 84
    private function executeUpserts(ClassMetadata $class, array $documents, array $options = [])
1089
    {
1090 84
        $persister = $this->getDocumentPersister($class->name);
1091
1092 84
        foreach ($documents as $oid => $document) {
1093 84
            $persister->addUpsert($document);
1094 84
            unset($this->documentUpserts[$oid]);
1095
        }
1096
1097 84
        $persister->executeUpserts($options);
1098
1099 84
        foreach ($documents as $document) {
1100 84
            $this->lifecycleEventManager->postPersist($class, $document);
1101
        }
1102 84
    }
1103
1104
    /**
1105
     * Executes all document updates for documents of the specified type.
1106
     *
1107
     * @param array $documents Array of documents to update
1108
     * @param array $options   Array of options to be used with update()
1109
     */
1110 205
    private function executeUpdates(ClassMetadata $class, array $documents, array $options = [])
1111
    {
1112 205
        if ($class->isReadOnly) {
1113
            return;
1114
        }
1115
1116 205
        $className = $class->name;
1117 205
        $persister = $this->getDocumentPersister($className);
1118
1119 205
        foreach ($documents as $oid => $document) {
1120 205
            $this->lifecycleEventManager->preUpdate($class, $document);
1121
1122 205
            if (! empty($this->documentChangeSets[$oid]) || $this->hasScheduledCollections($document)) {
1123 204
                $persister->update($document, $options);
1124
            }
1125
1126 201
            unset($this->documentUpdates[$oid]);
1127
1128 201
            $this->lifecycleEventManager->postUpdate($class, $document);
1129
        }
1130 201
    }
1131
1132
    /**
1133
     * Executes all document deletions for documents of the specified type.
1134
     *
1135
     * @param array $documents Array of documents to delete
1136
     * @param array $options   Array of options to be used with remove()
1137
     */
1138 64
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = [])
1139
    {
1140 64
        $persister = $this->getDocumentPersister($class->name);
1141
1142 64
        foreach ($documents as $oid => $document) {
1143 64
            if (! $class->isEmbeddedDocument) {
1144 32
                $persister->delete($document, $options);
1145
            }
1146
            unset(
1147 62
                $this->documentDeletions[$oid],
1148 62
                $this->documentIdentifiers[$oid],
1149 62
                $this->originalDocumentData[$oid]
1150
            );
1151
1152
            // Clear snapshot information for any referenced PersistentCollection
1153
            // http://www.doctrine-project.org/jira/browse/MODM-95
1154 62
            foreach ($class->associationMappings as $fieldMapping) {
1155 38
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1156 28
                    continue;
1157
                }
1158
1159 24
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1160 24
                if (! ($value instanceof PersistentCollectionInterface)) {
1161 7
                    continue;
1162
                }
1163
1164 20
                $value->clearSnapshot();
1165
            }
1166
1167
            // Document with this $oid after deletion treated as NEW, even if the $oid
1168
            // is obtained by a new document because the old one went out of scope.
1169 62
            $this->documentStates[$oid] = self::STATE_NEW;
1170
1171 62
            $this->lifecycleEventManager->postRemove($class, $document);
1172
        }
1173 62
    }
1174
1175
    /**
1176
     * Schedules a document for insertion into the database.
1177
     * If the document already has an identifier, it will be added to the
1178
     * identity map.
1179
     *
1180
     * @param object $document The document to schedule for insertion.
1181
     * @throws \InvalidArgumentException
1182
     */
1183 528
    public function scheduleForInsert(ClassMetadata $class, $document)
1184
    {
1185 528
        $oid = spl_object_hash($document);
1186
1187 528
        if (isset($this->documentUpdates[$oid])) {
1188
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1189
        }
1190 528
        if (isset($this->documentDeletions[$oid])) {
1191
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1192
        }
1193 528
        if (isset($this->documentInsertions[$oid])) {
1194
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1195
        }
1196
1197 528
        $this->documentInsertions[$oid] = $document;
1198
1199 528
        if (! isset($this->documentIdentifiers[$oid])) {
1200 3
            return;
1201
        }
1202
1203 525
        $this->addToIdentityMap($document);
1204 525
    }
1205
1206
    /**
1207
     * Schedules a document for upsert into the database and adds it to the
1208
     * identity map
1209
     *
1210
     * @param object $document The document to schedule for upsert.
1211
     * @throws \InvalidArgumentException
1212
     */
1213 90
    public function scheduleForUpsert(ClassMetadata $class, $document)
1214
    {
1215 90
        $oid = spl_object_hash($document);
1216
1217 90
        if ($class->isEmbeddedDocument) {
1218
            throw new \InvalidArgumentException('Embedded document can not be scheduled for upsert.');
1219
        }
1220 90
        if (isset($this->documentUpdates[$oid])) {
1221
            throw new \InvalidArgumentException('Dirty document can not be scheduled for upsert.');
1222
        }
1223 90
        if (isset($this->documentDeletions[$oid])) {
1224
            throw new \InvalidArgumentException('Removed document can not be scheduled for upsert.');
1225
        }
1226 90
        if (isset($this->documentUpserts[$oid])) {
1227
            throw new \InvalidArgumentException('Document can not be scheduled for upsert twice.');
1228
        }
1229
1230 90
        $this->documentUpserts[$oid] = $document;
1231 90
        $this->documentIdentifiers[$oid] = $class->getIdentifierValue($document);
1232 90
        $this->addToIdentityMap($document);
1233 90
    }
1234
1235
    /**
1236
     * Checks whether a document is scheduled for insertion.
1237
     *
1238
     * @param object $document
1239
     * @return bool
1240
     */
1241 89
    public function isScheduledForInsert($document)
1242
    {
1243 89
        return isset($this->documentInsertions[spl_object_hash($document)]);
1244
    }
1245
1246
    /**
1247
     * Checks whether a document is scheduled for upsert.
1248
     *
1249
     * @param object $document
1250
     * @return bool
1251
     */
1252 5
    public function isScheduledForUpsert($document)
1253
    {
1254 5
        return isset($this->documentUpserts[spl_object_hash($document)]);
1255
    }
1256
1257
    /**
1258
     * Schedules a document for being updated.
1259
     *
1260
     * @param object $document The document to schedule for being updated.
1261
     * @throws \InvalidArgumentException
1262
     */
1263 211
    public function scheduleForUpdate($document)
1264
    {
1265 211
        $oid = spl_object_hash($document);
1266 211
        if (! isset($this->documentIdentifiers[$oid])) {
1267
            throw new \InvalidArgumentException('Document has no identity.');
1268
        }
1269
1270 211
        if (isset($this->documentDeletions[$oid])) {
1271
            throw new \InvalidArgumentException('Document is removed.');
1272
        }
1273
1274 211
        if (isset($this->documentUpdates[$oid])
1275 211
            || isset($this->documentInsertions[$oid])
1276 211
            || isset($this->documentUpserts[$oid])) {
1277 79
            return;
1278
        }
1279
1280 210
        $this->documentUpdates[$oid] = $document;
1281 210
    }
1282
1283
    /**
1284
     * Checks whether a document is registered as dirty in the unit of work.
1285
     * Note: Is not very useful currently as dirty documents are only registered
1286
     * at commit time.
1287
     *
1288
     * @param object $document
1289
     * @return bool
1290
     */
1291 15
    public function isScheduledForUpdate($document)
1292
    {
1293 15
        return isset($this->documentUpdates[spl_object_hash($document)]);
1294
    }
1295
1296 1
    public function isScheduledForDirtyCheck($document)
1297
    {
1298 1
        $class = $this->dm->getClassMetadata(get_class($document));
1299 1
        return isset($this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)]);
1300
    }
1301
1302
    /**
1303
     * INTERNAL:
1304
     * Schedules a document for deletion.
1305
     *
1306
     * @param object $document
1307
     */
1308 69
    public function scheduleForDelete($document)
1309
    {
1310 69
        $oid = spl_object_hash($document);
1311
1312 69
        if (isset($this->documentInsertions[$oid])) {
1313 1
            if ($this->isInIdentityMap($document)) {
1314 1
                $this->removeFromIdentityMap($document);
1315
            }
1316 1
            unset($this->documentInsertions[$oid]);
1317 1
            return; // document has not been persisted yet, so nothing more to do.
1318
        }
1319
1320 68
        if (! $this->isInIdentityMap($document)) {
1321 2
            return; // ignore
1322
        }
1323
1324 67
        $this->removeFromIdentityMap($document);
1325 67
        $this->documentStates[$oid] = self::STATE_REMOVED;
1326
1327 67
        if (isset($this->documentUpdates[$oid])) {
1328
            unset($this->documentUpdates[$oid]);
1329
        }
1330 67
        if (isset($this->documentDeletions[$oid])) {
1331
            return;
1332
        }
1333
1334 67
        $this->documentDeletions[$oid] = $document;
1335 67
    }
1336
1337
    /**
1338
     * Checks whether a document is registered as removed/deleted with the unit
1339
     * of work.
1340
     *
1341
     * @param object $document
1342
     * @return bool
1343
     */
1344 5
    public function isScheduledForDelete($document)
1345
    {
1346 5
        return isset($this->documentDeletions[spl_object_hash($document)]);
1347
    }
1348
1349
    /**
1350
     * Checks whether a document is scheduled for insertion, update or deletion.
1351
     *
1352
     * @param object $document
1353
     * @return bool
1354
     */
1355 227
    public function isDocumentScheduled($document)
1356
    {
1357 227
        $oid = spl_object_hash($document);
1358 227
        return isset($this->documentInsertions[$oid]) ||
1359 113
            isset($this->documentUpserts[$oid]) ||
1360 104
            isset($this->documentUpdates[$oid]) ||
1361 227
            isset($this->documentDeletions[$oid]);
1362
    }
1363
1364
    /**
1365
     * INTERNAL:
1366
     * Registers a document in the identity map.
1367
     *
1368
     * Note that documents in a hierarchy are registered with the class name of
1369
     * the root document. Identifiers are serialized before being used as array
1370
     * keys to allow differentiation of equal, but not identical, values.
1371
     *
1372
     * @ignore
1373
     * @param object $document The document to register.
1374
     * @return bool  TRUE if the registration was successful, FALSE if the identity of
1375
     *                  the document in question is already managed.
1376
     */
1377 626
    public function addToIdentityMap($document)
1378
    {
1379 626
        $class = $this->dm->getClassMetadata(get_class($document));
1380 626
        $id = $this->getIdForIdentityMap($document);
1381
1382 626
        if (isset($this->identityMap[$class->name][$id])) {
1383 42
            return false;
1384
        }
1385
1386 626
        $this->identityMap[$class->name][$id] = $document;
1387
1388 626
        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...
1389 626
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1390 3
            $document->addPropertyChangedListener($this);
1391
        }
1392
1393 626
        return true;
1394
    }
1395
1396
    /**
1397
     * Gets the state of a document with regard to the current unit of work.
1398
     *
1399
     * @param object   $document
1400
     * @param int|null $assume   The state to assume if the state is not yet known (not MANAGED or REMOVED).
1401
     *                           This parameter can be set to improve performance of document state detection
1402
     *                           by potentially avoiding a database lookup if the distinction between NEW and DETACHED
1403
     *                           is either known or does not matter for the caller of the method.
1404
     * @return int The document state.
1405
     */
1406 598
    public function getDocumentState($document, $assume = null)
1407
    {
1408 598
        $oid = spl_object_hash($document);
1409
1410 598
        if (isset($this->documentStates[$oid])) {
1411 367
            return $this->documentStates[$oid];
1412
        }
1413
1414 598
        $class = $this->dm->getClassMetadata(get_class($document));
1415
1416 598
        if ($class->isEmbeddedDocument) {
1417 163
            return self::STATE_NEW;
1418
        }
1419
1420 595
        if ($assume !== null) {
1421 593
            return $assume;
1422
        }
1423
1424
        /* State can only be NEW or DETACHED, because MANAGED/REMOVED states are
1425
         * known. Note that you cannot remember the NEW or DETACHED state in
1426
         * _documentStates since the UoW does not hold references to such
1427
         * objects and the object hash can be reused. More generally, because
1428
         * the state may "change" between NEW/DETACHED without the UoW being
1429
         * aware of it.
1430
         */
1431 3
        $id = $class->getIdentifierObject($document);
1432
1433 3
        if ($id === null) {
1434 2
            return self::STATE_NEW;
1435
        }
1436
1437
        // Check for a version field, if available, to avoid a DB lookup.
1438 2
        if ($class->isVersioned) {
1439
            return $class->getFieldValue($document, $class->versionField)
1440
                ? self::STATE_DETACHED
1441
                : self::STATE_NEW;
1442
        }
1443
1444
        // Last try before DB lookup: check the identity map.
1445 2
        if ($this->tryGetById($id, $class)) {
1446 1
            return self::STATE_DETACHED;
1447
        }
1448
1449
        // DB lookup
1450 2
        if ($this->getDocumentPersister($class->name)->exists($document)) {
1451 1
            return self::STATE_DETACHED;
1452
        }
1453
1454 1
        return self::STATE_NEW;
1455
    }
1456
1457
    /**
1458
     * INTERNAL:
1459
     * Removes a document from the identity map. This effectively detaches the
1460
     * document from the persistence management of Doctrine.
1461
     *
1462
     * @ignore
1463
     * @param object $document
1464
     * @throws \InvalidArgumentException
1465
     * @return bool
1466
     */
1467 81
    public function removeFromIdentityMap($document)
1468
    {
1469 81
        $oid = spl_object_hash($document);
1470
1471
        // Check if id is registered first
1472 81
        if (! isset($this->documentIdentifiers[$oid])) {
1473
            return false;
1474
        }
1475
1476 81
        $class = $this->dm->getClassMetadata(get_class($document));
1477 81
        $id = $this->getIdForIdentityMap($document);
1478
1479 81
        if (isset($this->identityMap[$class->name][$id])) {
1480 81
            unset($this->identityMap[$class->name][$id]);
1481 81
            $this->documentStates[$oid] = self::STATE_DETACHED;
1482 81
            return true;
1483
        }
1484
1485
        return false;
1486
    }
1487
1488
    /**
1489
     * INTERNAL:
1490
     * Gets a document in the identity map by its identifier hash.
1491
     *
1492
     * @ignore
1493
     * @param mixed         $id    Document identifier
1494
     * @param ClassMetadata $class Document class
1495
     * @return object
1496
     * @throws InvalidArgumentException If the class does not have an identifier.
1497
     */
1498 39
    public function getById($id, ClassMetadata $class)
1499
    {
1500 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...
1501
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1502
        }
1503
1504 39
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1505
1506 39
        return $this->identityMap[$class->name][$serializedId];
1507
    }
1508
1509
    /**
1510
     * INTERNAL:
1511
     * Tries to get a document by its identifier hash. If no document is found
1512
     * for the given hash, FALSE is returned.
1513
     *
1514
     * @ignore
1515
     * @param mixed         $id    Document identifier
1516
     * @param ClassMetadata $class Document class
1517
     * @return mixed The found document or FALSE.
1518
     * @throws InvalidArgumentException If the class does not have an identifier.
1519
     */
1520 294
    public function tryGetById($id, ClassMetadata $class)
1521
    {
1522 294
        if (! $class->identifier) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->identifier of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

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

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

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1523
            throw new \InvalidArgumentException(sprintf('Class "%s" does not have an identifier', $class->name));
1524
        }
1525
1526 294
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1527
1528 294
        return $this->identityMap[$class->name][$serializedId] ?? false;
1529
    }
1530
1531
    /**
1532
     * Schedules a document for dirty-checking at commit-time.
1533
     *
1534
     * @param object $document The document to schedule for dirty-checking.
1535
     * @todo Rename: scheduleForSynchronization
1536
     */
1537 3
    public function scheduleForDirtyCheck($document)
1538
    {
1539 3
        $class = $this->dm->getClassMetadata(get_class($document));
1540 3
        $this->scheduledForDirtyCheck[$class->name][spl_object_hash($document)] = $document;
1541 3
    }
1542
1543
    /**
1544
     * Checks whether a document is registered in the identity map.
1545
     *
1546
     * @param object $document
1547
     * @return bool
1548
     */
1549 77
    public function isInIdentityMap($document)
1550
    {
1551 77
        $oid = spl_object_hash($document);
1552
1553 77
        if (! isset($this->documentIdentifiers[$oid])) {
1554 6
            return false;
1555
        }
1556
1557 75
        $class = $this->dm->getClassMetadata(get_class($document));
1558 75
        $id = $this->getIdForIdentityMap($document);
1559
1560 75
        return isset($this->identityMap[$class->name][$id]);
1561
    }
1562
1563
    /**
1564
     * @param object $document
1565
     * @return string
1566
     */
1567 626
    private function getIdForIdentityMap($document)
1568
    {
1569 626
        $class = $this->dm->getClassMetadata(get_class($document));
1570
1571 626
        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...
1572 133
            $id = spl_object_hash($document);
1573
        } else {
1574 625
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1575 625
            $id = serialize($class->getDatabaseIdentifierValue($id));
1576
        }
1577
1578 626
        return $id;
1579
    }
1580
1581
    /**
1582
     * INTERNAL:
1583
     * Checks whether an identifier exists in the identity map.
1584
     *
1585
     * @ignore
1586
     * @param string $id
1587
     * @param string $rootClassName
1588
     * @return bool
1589
     */
1590
    public function containsId($id, $rootClassName)
1591
    {
1592
        return isset($this->identityMap[$rootClassName][serialize($id)]);
1593
    }
1594
1595
    /**
1596
     * Persists a document as part of the current unit of work.
1597
     *
1598
     * @param object $document The document to persist.
1599
     * @throws MongoDBException If trying to persist MappedSuperclass.
1600
     * @throws \InvalidArgumentException If there is something wrong with document's identifier.
1601
     */
1602 596
    public function persist($document)
1603
    {
1604 596
        $class = $this->dm->getClassMetadata(get_class($document));
1605 596
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1606 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1607
        }
1608 595
        $visited = [];
1609 595
        $this->doPersist($document, $visited);
1610 591
    }
1611
1612
    /**
1613
     * Saves a document as part of the current unit of work.
1614
     * This method is internally called during save() cascades as it tracks
1615
     * the already visited documents to prevent infinite recursions.
1616
     *
1617
     * NOTE: This method always considers documents that are not yet known to
1618
     * this UnitOfWork as NEW.
1619
     *
1620
     * @param object $document The document to persist.
1621
     * @param array  $visited  The already visited documents.
1622
     * @throws \InvalidArgumentException
1623
     * @throws MongoDBException
1624
     */
1625 595
    private function doPersist($document, array &$visited)
1626
    {
1627 595
        $oid = spl_object_hash($document);
1628 595
        if (isset($visited[$oid])) {
1629 25
            return; // Prevent infinite recursion
1630
        }
1631
1632 595
        $visited[$oid] = $document; // Mark visited
1633
1634 595
        $class = $this->dm->getClassMetadata(get_class($document));
1635
1636 595
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1637
        switch ($documentState) {
1638 595
            case self::STATE_MANAGED:
1639
                // Nothing to do, except if policy is "deferred explicit"
1640 51
                if ($class->isChangeTrackingDeferredExplicit()) {
1641
                    $this->scheduleForDirtyCheck($document);
1642
                }
1643 51
                break;
1644 595
            case self::STATE_NEW:
1645 595
                $this->persistNew($class, $document);
1646 593
                break;
1647
1648 2
            case self::STATE_REMOVED:
1649
                // Document becomes managed again
1650 2
                unset($this->documentDeletions[$oid]);
1651
1652 2
                $this->documentStates[$oid] = self::STATE_MANAGED;
1653 2
                break;
1654
1655
            case self::STATE_DETACHED:
1656
                throw new \InvalidArgumentException(
1657
                    'Behavior of persist() for a detached document is not yet defined.'
1658
                );
1659
1660
            default:
1661
                throw MongoDBException::invalidDocumentState($documentState);
1662
        }
1663
1664 593
        $this->cascadePersist($document, $visited);
1665 591
    }
1666
1667
    /**
1668
     * Deletes a document as part of the current unit of work.
1669
     *
1670
     * @param object $document The document to remove.
1671
     */
1672 68
    public function remove($document)
1673
    {
1674 68
        $visited = [];
1675 68
        $this->doRemove($document, $visited);
1676 68
    }
1677
1678
    /**
1679
     * Deletes a document as part of the current unit of work.
1680
     *
1681
     * This method is internally called during delete() cascades as it tracks
1682
     * the already visited documents to prevent infinite recursions.
1683
     *
1684
     * @param object $document The document to delete.
1685
     * @param array  $visited  The map of the already visited documents.
1686
     * @throws MongoDBException
1687
     */
1688 68
    private function doRemove($document, array &$visited)
1689
    {
1690 68
        $oid = spl_object_hash($document);
1691 68
        if (isset($visited[$oid])) {
1692 1
            return; // Prevent infinite recursion
1693
        }
1694
1695 68
        $visited[$oid] = $document; // mark visited
1696
1697
        /* Cascade first, because scheduleForDelete() removes the entity from
1698
         * the identity map, which can cause problems when a lazy Proxy has to
1699
         * be initialized for the cascade operation.
1700
         */
1701 68
        $this->cascadeRemove($document, $visited);
1702
1703 68
        $class = $this->dm->getClassMetadata(get_class($document));
1704 68
        $documentState = $this->getDocumentState($document);
1705
        switch ($documentState) {
1706 68
            case self::STATE_NEW:
1707 68
            case self::STATE_REMOVED:
1708
                // nothing to do
1709
                break;
1710 68
            case self::STATE_MANAGED:
1711 68
                $this->lifecycleEventManager->preRemove($class, $document);
1712 68
                $this->scheduleForDelete($document);
1713 68
                break;
1714
            case self::STATE_DETACHED:
1715
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1716
            default:
1717
                throw MongoDBException::invalidDocumentState($documentState);
1718
        }
1719 68
    }
1720
1721
    /**
1722
     * Merges the state of the given detached document into this UnitOfWork.
1723
     *
1724
     * @param object $document
1725
     * @return object The managed copy of the document.
1726
     */
1727 12
    public function merge($document)
1728
    {
1729 12
        $visited = [];
1730
1731 12
        return $this->doMerge($document, $visited);
1732
    }
1733
1734
    /**
1735
     * Executes a merge operation on a document.
1736
     *
1737
     * @param object      $document
1738
     * @param array       $visited
1739
     * @param object|null $prevManagedCopy
1740
     * @param array|null  $assoc
1741
     *
1742
     * @return object The managed copy of the document.
1743
     *
1744
     * @throws InvalidArgumentException If the entity instance is NEW.
1745
     * @throws LockException If the document uses optimistic locking through a
1746
     *                       version attribute and the version check against the
1747
     *                       managed copy fails.
1748
     */
1749 12
    private function doMerge($document, array &$visited, $prevManagedCopy = null, $assoc = null)
1750
    {
1751 12
        $oid = spl_object_hash($document);
1752
1753 12
        if (isset($visited[$oid])) {
1754 1
            return $visited[$oid]; // Prevent infinite recursion
1755
        }
1756
1757 12
        $visited[$oid] = $document; // mark visited
1758
1759 12
        $class = $this->dm->getClassMetadata(get_class($document));
1760
1761
        /* First we assume DETACHED, although it can still be NEW but we can
1762
         * avoid an extra DB round trip this way. If it is not MANAGED but has
1763
         * an identity, we need to fetch it from the DB anyway in order to
1764
         * merge. MANAGED documents are ignored by the merge operation.
1765
         */
1766 12
        $managedCopy = $document;
1767
1768 12
        if ($this->getDocumentState($document, self::STATE_DETACHED) !== self::STATE_MANAGED) {
1769 12
            if ($document instanceof Proxy && ! $document->__isInitialized()) {
1770
                $document->__load();
1771
            }
1772
1773 12
            $identifier = $class->getIdentifier();
1774
            // We always have one element in the identifier array but it might be null
1775 12
            $id = $identifier[0] !== null ? $class->getIdentifierObject($document) : null;
1776 12
            $managedCopy = null;
1777
1778
            // Try to fetch document from the database
1779 12
            if (! $class->isEmbeddedDocument && $id !== null) {
1780 12
                $managedCopy = $this->dm->find($class->name, $id);
1781
1782
                // Managed copy may be removed in which case we can't merge
1783 12
                if ($managedCopy && $this->getDocumentState($managedCopy) === self::STATE_REMOVED) {
1784
                    throw new \InvalidArgumentException('Removed entity detected during merge. Cannot merge with a removed entity.');
1785
                }
1786
1787 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...
1788
                    $managedCopy->__load();
1789
                }
1790
            }
1791
1792 12
            if ($managedCopy === null) {
1793
                // Create a new managed instance
1794 4
                $managedCopy = $class->newInstance();
1795 4
                if ($id !== null) {
1796 3
                    $class->setIdentifierValue($managedCopy, $id);
1797
                }
1798 4
                $this->persistNew($class, $managedCopy);
1799
            }
1800
1801 12
            if ($class->isVersioned) {
1802
                $managedCopyVersion = $class->reflFields[$class->versionField]->getValue($managedCopy);
1803
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
1804
1805
                // Throw exception if versions don't match
1806
                if ($managedCopyVersion !== $documentVersion) {
1807
                    throw LockException::lockFailedVersionMissmatch($document, $documentVersion, $managedCopyVersion);
1808
                }
1809
            }
1810
1811
            // Merge state of $document into existing (managed) document
1812 12
            foreach ($class->reflClass->getProperties() as $prop) {
1813 12
                $name = $prop->name;
1814 12
                $prop->setAccessible(true);
1815 12
                if (! isset($class->associationMappings[$name])) {
1816 12
                    if (! $class->isIdentifier($name)) {
1817 12
                        $prop->setValue($managedCopy, $prop->getValue($document));
1818
                    }
1819
                } else {
1820 12
                    $assoc2 = $class->associationMappings[$name];
1821
1822 12
                    if ($assoc2['type'] === 'one') {
1823 6
                        $other = $prop->getValue($document);
1824
1825 6
                        if ($other === null) {
1826 2
                            $prop->setValue($managedCopy, null);
1827 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...
1828
                            // Do not merge fields marked lazy that have not been fetched
1829 1
                            continue;
1830 4
                        } elseif (! $assoc2['isCascadeMerge']) {
1831
                            if ($this->getDocumentState($other) === self::STATE_DETACHED) {
1832
                                $targetDocument = $assoc2['targetDocument'] ?? get_class($other);
1833
                                /* @var $targetClass \Doctrine\ODM\MongoDB\Mapping\ClassMetadata */
1834
                                $targetClass = $this->dm->getClassMetadata($targetDocument);
1835
                                $relatedId = $targetClass->getIdentifierObject($other);
1836
1837
                                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...
1838
                                    $other = $this->dm->find($targetClass->name, $relatedId);
1839
                                } else {
1840
                                    $other = $this
1841
                                        ->dm
1842
                                        ->getProxyFactory()
1843
                                        ->getProxy($assoc2['targetDocument'], [$targetClass->identifier => $relatedId]);
1844
                                    $this->registerManaged($other, $relatedId, []);
1845
                                }
1846
                            }
1847
1848 5
                            $prop->setValue($managedCopy, $other);
1849
                        }
1850
                    } else {
1851 10
                        $mergeCol = $prop->getValue($document);
1852
1853 10
                        if ($mergeCol instanceof PersistentCollectionInterface && ! $mergeCol->isInitialized() && ! $assoc2['isCascadeMerge']) {
1854
                            /* Do not merge fields marked lazy that have not
1855
                             * been fetched. Keep the lazy persistent collection
1856
                             * of the managed copy.
1857
                             */
1858 3
                            continue;
1859
                        }
1860
1861 10
                        $managedCol = $prop->getValue($managedCopy);
1862
1863 10
                        if (! $managedCol) {
1864 1
                            $managedCol = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $assoc2, null);
1865 1
                            $managedCol->setOwner($managedCopy, $assoc2);
1866 1
                            $prop->setValue($managedCopy, $managedCol);
1867 1
                            $this->originalDocumentData[$oid][$name] = $managedCol;
1868
                        }
1869
1870
                        /* Note: do not process association's target documents.
1871
                         * They will be handled during the cascade. Initialize
1872
                         * and, if necessary, clear $managedCol for now.
1873
                         */
1874 10
                        if ($assoc2['isCascadeMerge']) {
1875 10
                            $managedCol->initialize();
1876
1877
                            // If $managedCol differs from the merged collection, clear and set dirty
1878 10
                            if (! $managedCol->isEmpty() && $managedCol !== $mergeCol) {
1879 3
                                $managedCol->unwrap()->clear();
1880 3
                                $managedCol->setDirty(true);
1881
1882 3
                                if ($assoc2['isOwningSide'] && $class->isChangeTrackingNotify()) {
1883
                                    $this->scheduleForDirtyCheck($managedCopy);
1884
                                }
1885
                            }
1886
                        }
1887
                    }
1888
                }
1889
1890 12
                if (! $class->isChangeTrackingNotify()) {
1891 12
                    continue;
1892
                }
1893
1894
                // Just treat all properties as changed, there is no other choice.
1895
                $this->propertyChanged($managedCopy, $name, null, $prop->getValue($managedCopy));
1896
            }
1897
1898 12
            if ($class->isChangeTrackingDeferredExplicit()) {
1899
                $this->scheduleForDirtyCheck($document);
1900
            }
1901
        }
1902
1903 12
        if ($prevManagedCopy !== null) {
1904 5
            $assocField = $assoc['fieldName'];
1905 5
            $prevClass = $this->dm->getClassMetadata(get_class($prevManagedCopy));
1906
1907 5
            if ($assoc['type'] === 'one') {
1908 3
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1909
            } else {
1910 4
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1911
1912 4
                if ($assoc['type'] === 'many' && isset($assoc['mappedBy'])) {
1913 1
                    $class->reflFields[$assoc['mappedBy']]->setValue($managedCopy, $prevManagedCopy);
1914
                }
1915
            }
1916
        }
1917
1918
        // Mark the managed copy visited as well
1919 12
        $visited[spl_object_hash($managedCopy)] = true;
1920
1921 12
        $this->cascadeMerge($document, $managedCopy, $visited);
1922
1923 12
        return $managedCopy;
1924
    }
1925
1926
    /**
1927
     * Detaches a document from the persistence management. It's persistence will
1928
     * no longer be managed by Doctrine.
1929
     *
1930
     * @param object $document The document to detach.
1931
     */
1932 11
    public function detach($document)
1933
    {
1934 11
        $visited = [];
1935 11
        $this->doDetach($document, $visited);
1936 11
    }
1937
1938
    /**
1939
     * Executes a detach operation on the given document.
1940
     *
1941
     * @param object $document
1942
     * @param array  $visited
1943
     * @internal This method always considers documents with an assigned identifier as DETACHED.
1944
     */
1945 16
    private function doDetach($document, array &$visited)
1946
    {
1947 16
        $oid = spl_object_hash($document);
1948 16
        if (isset($visited[$oid])) {
1949 3
            return; // Prevent infinite recursion
1950
        }
1951
1952 16
        $visited[$oid] = $document; // mark visited
1953
1954 16
        switch ($this->getDocumentState($document, self::STATE_DETACHED)) {
1955 16
            case self::STATE_MANAGED:
1956 16
                $this->removeFromIdentityMap($document);
1957
                unset(
1958 16
                    $this->documentInsertions[$oid],
1959 16
                    $this->documentUpdates[$oid],
1960 16
                    $this->documentDeletions[$oid],
1961 16
                    $this->documentIdentifiers[$oid],
1962 16
                    $this->documentStates[$oid],
1963 16
                    $this->originalDocumentData[$oid],
1964 16
                    $this->parentAssociations[$oid],
1965 16
                    $this->documentUpserts[$oid],
1966 16
                    $this->hasScheduledCollections[$oid],
1967 16
                    $this->embeddedDocumentsRegistry[$oid]
1968
                );
1969 16
                break;
1970 3
            case self::STATE_NEW:
1971 3
            case self::STATE_DETACHED:
1972 3
                return;
1973
        }
1974
1975 16
        $this->cascadeDetach($document, $visited);
1976 16
    }
1977
1978
    /**
1979
     * Refreshes the state of the given document from the database, overwriting
1980
     * any local, unpersisted changes.
1981
     *
1982
     * @param object $document The document to refresh.
1983
     * @throws \InvalidArgumentException If the document is not MANAGED.
1984
     */
1985 21
    public function refresh($document)
1986
    {
1987 21
        $visited = [];
1988 21
        $this->doRefresh($document, $visited);
1989 20
    }
1990
1991
    /**
1992
     * Executes a refresh operation on a document.
1993
     *
1994
     * @param object $document The document to refresh.
1995
     * @param array  $visited  The already visited documents during cascades.
1996
     * @throws \InvalidArgumentException If the document is not MANAGED.
1997
     */
1998 21
    private function doRefresh($document, array &$visited)
1999
    {
2000 21
        $oid = spl_object_hash($document);
2001 21
        if (isset($visited[$oid])) {
2002
            return; // Prevent infinite recursion
2003
        }
2004
2005 21
        $visited[$oid] = $document; // mark visited
2006
2007 21
        $class = $this->dm->getClassMetadata(get_class($document));
2008
2009 21
        if (! $class->isEmbeddedDocument) {
2010 21
            if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2011 1
                throw new \InvalidArgumentException('Document is not MANAGED.');
2012
            }
2013
2014 20
            $this->getDocumentPersister($class->name)->refresh($document);
2015
        }
2016
2017 20
        $this->cascadeRefresh($document, $visited);
2018 20
    }
2019
2020
    /**
2021
     * Cascades a refresh operation to associated documents.
2022
     *
2023
     * @param object $document
2024
     * @param array  $visited
2025
     */
2026 20
    private function cascadeRefresh($document, array &$visited)
2027
    {
2028 20
        $class = $this->dm->getClassMetadata(get_class($document));
2029
2030 20
        $associationMappings = array_filter(
2031 20
            $class->associationMappings,
2032
            function ($assoc) {
2033 17
                return $assoc['isCascadeRefresh'];
2034 20
            }
2035
        );
2036
2037 20
        foreach ($associationMappings as $mapping) {
2038 15
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2039 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...
2040 15
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2041
                    // Unwrap so that foreach() does not initialize
2042 15
                    $relatedDocuments = $relatedDocuments->unwrap();
2043
                }
2044 15
                foreach ($relatedDocuments as $relatedDocument) {
2045 15
                    $this->doRefresh($relatedDocument, $visited);
2046
                }
2047 10
            } elseif ($relatedDocuments !== null) {
2048 15
                $this->doRefresh($relatedDocuments, $visited);
2049
            }
2050
        }
2051 20
    }
2052
2053
    /**
2054
     * Cascades a detach operation to associated documents.
2055
     *
2056
     * @param object $document
2057
     * @param array  $visited
2058
     */
2059 16
    private function cascadeDetach($document, array &$visited)
2060
    {
2061 16
        $class = $this->dm->getClassMetadata(get_class($document));
2062 16
        foreach ($class->fieldMappings as $mapping) {
2063 16
            if (! $mapping['isCascadeDetach']) {
2064 16
                continue;
2065
            }
2066 10
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2067 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...
2068 10
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2069
                    // Unwrap so that foreach() does not initialize
2070 7
                    $relatedDocuments = $relatedDocuments->unwrap();
2071
                }
2072 10
                foreach ($relatedDocuments as $relatedDocument) {
2073 10
                    $this->doDetach($relatedDocument, $visited);
2074
                }
2075 10
            } elseif ($relatedDocuments !== null) {
2076 10
                $this->doDetach($relatedDocuments, $visited);
2077
            }
2078
        }
2079 16
    }
2080
    /**
2081
     * Cascades a merge operation to associated documents.
2082
     *
2083
     * @param object $document
2084
     * @param object $managedCopy
2085
     * @param array  $visited
2086
     */
2087 12
    private function cascadeMerge($document, $managedCopy, array &$visited)
2088
    {
2089 12
        $class = $this->dm->getClassMetadata(get_class($document));
2090
2091 12
        $associationMappings = array_filter(
2092 12
            $class->associationMappings,
2093
            function ($assoc) {
2094 12
                return $assoc['isCascadeMerge'];
2095 12
            }
2096
        );
2097
2098 12
        foreach ($associationMappings as $assoc) {
2099 11
            $relatedDocuments = $class->reflFields[$assoc['fieldName']]->getValue($document);
2100
2101 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...
2102 8
                if ($relatedDocuments === $class->reflFields[$assoc['fieldName']]->getValue($managedCopy)) {
2103
                    // Collections are the same, so there is nothing to do
2104 1
                    continue;
2105
                }
2106
2107 8
                foreach ($relatedDocuments as $relatedDocument) {
2108 8
                    $this->doMerge($relatedDocument, $visited, $managedCopy, $assoc);
2109
                }
2110 6
            } elseif ($relatedDocuments !== null) {
2111 11
                $this->doMerge($relatedDocuments, $visited, $managedCopy, $assoc);
2112
            }
2113
        }
2114 12
    }
2115
2116
    /**
2117
     * Cascades the save operation to associated documents.
2118
     *
2119
     * @param object $document
2120
     * @param array  $visited
2121
     */
2122 593
    private function cascadePersist($document, array &$visited)
2123
    {
2124 593
        $class = $this->dm->getClassMetadata(get_class($document));
2125
2126 593
        $associationMappings = array_filter(
2127 593
            $class->associationMappings,
2128
            function ($assoc) {
2129 451
                return $assoc['isCascadePersist'];
2130 593
            }
2131
        );
2132
2133 593
        foreach ($associationMappings as $fieldName => $mapping) {
2134 409
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2135
2136 409
            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...
2137 338
                if ($relatedDocuments instanceof PersistentCollectionInterface) {
2138 12
                    if ($relatedDocuments->getOwner() !== $document) {
2139 2
                        $relatedDocuments = $this->fixPersistentCollectionOwnership($relatedDocuments, $document, $class, $mapping['fieldName']);
2140
                    }
2141
                    // Unwrap so that foreach() does not initialize
2142 12
                    $relatedDocuments = $relatedDocuments->unwrap();
2143
                }
2144
2145 338
                $count = 0;
2146 338
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2147 174
                    if (! empty($mapping['embedded'])) {
2148 103
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2149 103
                        if ($knownParent && $knownParent !== $document) {
2150 1
                            $relatedDocument = clone $relatedDocument;
2151 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2152
                        }
2153 103
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2154 103
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2155
                    }
2156 338
                    $this->doPersist($relatedDocument, $visited);
2157
                }
2158 324
            } elseif ($relatedDocuments !== null) {
2159 128
                if (! empty($mapping['embedded'])) {
2160 67
                    list(, $knownParent, ) = $this->getParentAssociation($relatedDocuments);
2161 67
                    if ($knownParent && $knownParent !== $document) {
2162 3
                        $relatedDocuments = clone $relatedDocuments;
2163 3
                        $class->setFieldValue($document, $mapping['fieldName'], $relatedDocuments);
2164
                    }
2165 67
                    $this->setParentAssociation($relatedDocuments, $mapping, $document, $mapping['fieldName']);
2166
                }
2167 409
                $this->doPersist($relatedDocuments, $visited);
2168
            }
2169
        }
2170 591
    }
2171
2172
    /**
2173
     * Cascades the delete operation to associated documents.
2174
     *
2175
     * @param object $document
2176
     * @param array  $visited
2177
     */
2178 68
    private function cascadeRemove($document, array &$visited)
2179
    {
2180 68
        $class = $this->dm->getClassMetadata(get_class($document));
2181 68
        foreach ($class->fieldMappings as $mapping) {
2182 68
            if (! $mapping['isCascadeRemove']) {
2183 67
                continue;
2184
            }
2185 33
            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...
2186 2
                $document->__load();
2187
            }
2188
2189 33
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2190 33
            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...
2191
                // If its a PersistentCollection initialization is intended! No unwrap!
2192 22
                foreach ($relatedDocuments as $relatedDocument) {
2193 22
                    $this->doRemove($relatedDocument, $visited);
2194
                }
2195 22
            } elseif ($relatedDocuments !== null) {
2196 33
                $this->doRemove($relatedDocuments, $visited);
2197
            }
2198
        }
2199 68
    }
2200
2201
    /**
2202
     * Acquire a lock on the given document.
2203
     *
2204
     * @param object $document
2205
     * @param int    $lockMode
2206
     * @param int    $lockVersion
2207
     * @throws LockException
2208
     * @throws \InvalidArgumentException
2209
     */
2210 8
    public function lock($document, $lockMode, $lockVersion = null)
2211
    {
2212 8
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2213 1
            throw new \InvalidArgumentException('Document is not MANAGED.');
2214
        }
2215
2216 7
        $documentName = get_class($document);
2217 7
        $class = $this->dm->getClassMetadata($documentName);
2218
2219 7
        if ($lockMode === LockMode::OPTIMISTIC) {
2220 2
            if (! $class->isVersioned) {
2221 1
                throw LockException::notVersioned($documentName);
2222
            }
2223
2224 1
            if ($lockVersion !== null) {
2225 1
                $documentVersion = $class->reflFields[$class->versionField]->getValue($document);
2226 1
                if ($documentVersion !== $lockVersion) {
2227 1
                    throw LockException::lockFailedVersionMissmatch($document, $lockVersion, $documentVersion);
2228
                }
2229
            }
2230 5
        } elseif (in_array($lockMode, [LockMode::PESSIMISTIC_READ, LockMode::PESSIMISTIC_WRITE])) {
2231 5
            $this->getDocumentPersister($class->name)->lock($document, $lockMode);
2232
        }
2233 5
    }
2234
2235
    /**
2236
     * Releases a lock on the given document.
2237
     *
2238
     * @param object $document
2239
     * @throws \InvalidArgumentException
2240
     */
2241 1
    public function unlock($document)
2242
    {
2243 1
        if ($this->getDocumentState($document) !== self::STATE_MANAGED) {
2244
            throw new \InvalidArgumentException('Document is not MANAGED.');
2245
        }
2246 1
        $documentName = get_class($document);
2247 1
        $this->getDocumentPersister($documentName)->unlock($document);
2248 1
    }
2249
2250
    /**
2251
     * Clears the UnitOfWork.
2252
     *
2253
     * @param string|null $documentName if given, only documents of this type will get detached.
2254
     */
2255 372
    public function clear($documentName = null)
2256
    {
2257 372
        if ($documentName === null) {
2258 364
            $this->identityMap =
2259 364
            $this->documentIdentifiers =
2260 364
            $this->originalDocumentData =
2261 364
            $this->documentChangeSets =
2262 364
            $this->documentStates =
2263 364
            $this->scheduledForDirtyCheck =
2264 364
            $this->documentInsertions =
2265 364
            $this->documentUpserts =
2266 364
            $this->documentUpdates =
2267 364
            $this->documentDeletions =
2268 364
            $this->collectionUpdates =
2269 364
            $this->collectionDeletions =
2270 364
            $this->parentAssociations =
2271 364
            $this->embeddedDocumentsRegistry =
2272 364
            $this->orphanRemovals =
2273 364
            $this->hasScheduledCollections = [];
2274
        } else {
2275 8
            $visited = [];
2276 8
            foreach ($this->identityMap as $className => $documents) {
2277 8
                if ($className !== $documentName) {
2278 5
                    continue;
2279
                }
2280
2281 5
                foreach ($documents as $document) {
2282 5
                    $this->doDetach($document, $visited);
2283
                }
2284
            }
2285
        }
2286
2287 372
        if (! $this->evm->hasListeners(Events::onClear)) {
2288 372
            return;
2289
        }
2290
2291
        $this->evm->dispatchEvent(Events::onClear, new Event\OnClearEventArgs($this->dm, $documentName));
2292
    }
2293
2294
    /**
2295
     * INTERNAL:
2296
     * Schedules an embedded document for removal. The remove() operation will be
2297
     * invoked on that document at the beginning of the next commit of this
2298
     * UnitOfWork.
2299
     *
2300
     * @ignore
2301
     * @param object $document
2302
     */
2303 47
    public function scheduleOrphanRemoval($document)
2304
    {
2305 47
        $this->orphanRemovals[spl_object_hash($document)] = $document;
2306 47
    }
2307
2308
    /**
2309
     * INTERNAL:
2310
     * Unschedules an embedded or referenced object for removal.
2311
     *
2312
     * @ignore
2313
     * @param object $document
2314
     */
2315 100
    public function unscheduleOrphanRemoval($document)
2316
    {
2317 100
        $oid = spl_object_hash($document);
2318 100
        unset($this->orphanRemovals[$oid]);
2319 100
    }
2320
2321
    /**
2322
     * Fixes PersistentCollection state if it wasn't used exactly as we had in mind:
2323
     *  1) sets owner if it was cloned
2324
     *  2) clones collection, sets owner, updates document's property and, if necessary, updates originalData
2325
     *  3) NOP if state is OK
2326
     * Returned collection should be used from now on (only important with 2nd point)
2327
     *
2328
     * @param object $document
2329
     * @param string $propName
2330
     * @return PersistentCollectionInterface
2331
     */
2332 8
    private function fixPersistentCollectionOwnership(PersistentCollectionInterface $coll, $document, ClassMetadata $class, $propName)
2333
    {
2334 8
        $owner = $coll->getOwner();
2335 8
        if ($owner === null) { // cloned
2336 6
            $coll->setOwner($document, $class->fieldMappings[$propName]);
2337 2
        } elseif ($owner !== $document) { // no clone, we have to fix
2338 2
            if (! $coll->isInitialized()) {
2339 1
                $coll->initialize(); // we have to do this otherwise the cols share state
2340
            }
2341 2
            $newValue = clone $coll;
2342 2
            $newValue->setOwner($document, $class->fieldMappings[$propName]);
2343 2
            $class->reflFields[$propName]->setValue($document, $newValue);
2344 2
            if ($this->isScheduledForUpdate($document)) {
2345
                // @todo following line should be superfluous once collections are stored in change sets
2346
                $this->setOriginalDocumentProperty(spl_object_hash($document), $propName, $newValue);
2347
            }
2348 2
            return $newValue;
2349
        }
2350 6
        return $coll;
2351
    }
2352
2353
    /**
2354
     * INTERNAL:
2355
     * Schedules a complete collection for removal when this UnitOfWork commits.
2356
     *
2357
     */
2358 35
    public function scheduleCollectionDeletion(PersistentCollectionInterface $coll)
2359
    {
2360 35
        $oid = spl_object_hash($coll);
2361 35
        unset($this->collectionUpdates[$oid]);
2362 35
        if (isset($this->collectionDeletions[$oid])) {
2363
            return;
2364
        }
2365
2366 35
        $this->collectionDeletions[$oid] = $coll;
2367 35
        $this->scheduleCollectionOwner($coll);
2368 35
    }
2369
2370
    /**
2371
     * Checks whether a PersistentCollection is scheduled for deletion.
2372
     *
2373
     * @return bool
2374
     */
2375 191
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2376
    {
2377 191
        return isset($this->collectionDeletions[spl_object_hash($coll)]);
2378
    }
2379
2380
    /**
2381
     * INTERNAL:
2382
     * Unschedules a collection from being deleted when this UnitOfWork commits.
2383
     *
2384
     */
2385 203
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
2386
    {
2387 203
        $oid = spl_object_hash($coll);
2388 203
        if (! isset($this->collectionDeletions[$oid])) {
2389 203
            return;
2390
        }
2391
2392 5
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2393 5
        unset($this->collectionDeletions[$oid]);
2394 5
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2395 5
    }
2396
2397
    /**
2398
     * INTERNAL:
2399
     * Schedules a collection for update when this UnitOfWork commits.
2400
     *
2401
     */
2402 224
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2403
    {
2404 224
        $mapping = $coll->getMapping();
2405 224
        if (CollectionHelper::usesSet($mapping['strategy'])) {
2406
            /* There is no need to $unset collection if it will be $set later
2407
             * This is NOP if collection is not scheduled for deletion
2408
             */
2409 23
            $this->unscheduleCollectionDeletion($coll);
2410
        }
2411 224
        $oid = spl_object_hash($coll);
2412 224
        if (isset($this->collectionUpdates[$oid])) {
2413 7
            return;
2414
        }
2415
2416 224
        $this->collectionUpdates[$oid] = $coll;
2417 224
        $this->scheduleCollectionOwner($coll);
2418 224
    }
2419
2420
    /**
2421
     * INTERNAL:
2422
     * Unschedules a collection from being updated when this UnitOfWork commits.
2423
     *
2424
     */
2425 203
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
2426
    {
2427 203
        $oid = spl_object_hash($coll);
2428 203
        if (! isset($this->collectionUpdates[$oid])) {
2429 35
            return;
2430
        }
2431
2432 193
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2433 193
        unset($this->collectionUpdates[$oid]);
2434 193
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2435 193
    }
2436
2437
    /**
2438
     * Checks whether a PersistentCollection is scheduled for update.
2439
     *
2440
     * @return bool
2441
     */
2442 114
    public function isCollectionScheduledForUpdate(PersistentCollectionInterface $coll)
2443
    {
2444 114
        return isset($this->collectionUpdates[spl_object_hash($coll)]);
2445
    }
2446
2447
    /**
2448
     * INTERNAL:
2449
     * Gets PersistentCollections that have been visited during computing change
2450
     * set of $document
2451
     *
2452
     * @param object $document
2453
     * @return PersistentCollectionInterface[]
2454
     */
2455 547
    public function getVisitedCollections($document)
2456
    {
2457 547
        $oid = spl_object_hash($document);
2458
2459 547
        return $this->visitedCollections[$oid] ?? [];
2460
    }
2461
2462
    /**
2463
     * INTERNAL:
2464
     * Gets PersistentCollections that are scheduled to update and related to $document
2465
     *
2466
     * @param object $document
2467
     * @return array
2468
     */
2469 547
    public function getScheduledCollections($document)
2470
    {
2471 547
        $oid = spl_object_hash($document);
2472
2473 547
        return $this->hasScheduledCollections[$oid] ?? [];
2474
    }
2475
2476
    /**
2477
     * Checks whether the document is related to a PersistentCollection
2478
     * scheduled for update or deletion.
2479
     *
2480
     * @param object $document
2481
     * @return bool
2482
     */
2483 44
    public function hasScheduledCollections($document)
2484
    {
2485 44
        return isset($this->hasScheduledCollections[spl_object_hash($document)]);
2486
    }
2487
2488
    /**
2489
     * Marks the PersistentCollection's top-level owner as having a relation to
2490
     * a collection scheduled for update or deletion.
2491
     *
2492
     * If the owner is not scheduled for any lifecycle action, it will be
2493
     * scheduled for update to ensure that versioning takes place if necessary.
2494
     *
2495
     * If the collection is nested within atomic collection, it is immediately
2496
     * unscheduled and atomic one is scheduled for update instead. This makes
2497
     * calculating update data way easier.
2498
     *
2499
     */
2500 226
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2501
    {
2502 226
        $document = $this->getOwningDocument($coll->getOwner());
2503 226
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2504
2505 226
        if ($document !== $coll->getOwner()) {
2506 19
            $parent = $coll->getOwner();
2507 19
            $mapping = [];
2508 19
            while (($parentAssoc = $this->getParentAssociation($parent)) !== null) {
2509 19
                list($mapping, $parent, ) = $parentAssoc;
2510
            }
2511 19
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
2512 3
                $class = $this->dm->getClassMetadata(get_class($document));
2513 3
                $atomicCollection = $class->getFieldValue($document, $mapping['fieldName']);
2514 3
                $this->scheduleCollectionUpdate($atomicCollection);
2515 3
                $this->unscheduleCollectionDeletion($coll);
2516 3
                $this->unscheduleCollectionUpdate($coll);
2517
            }
2518
        }
2519
2520 226
        if ($this->isDocumentScheduled($document)) {
2521 221
            return;
2522
        }
2523
2524 39
        $this->scheduleForUpdate($document);
2525 39
    }
2526
2527
    /**
2528
     * Get the top-most owning document of a given document
2529
     *
2530
     * If a top-level document is provided, that same document will be returned.
2531
     * For an embedded document, we will walk through parent associations until
2532
     * we find a top-level document.
2533
     *
2534
     * @param object $document
2535
     * @throws \UnexpectedValueException When a top-level document could not be found.
2536
     * @return object
2537
     */
2538 228
    public function getOwningDocument($document)
2539
    {
2540 228
        $class = $this->dm->getClassMetadata(get_class($document));
2541 228
        while ($class->isEmbeddedDocument) {
2542 33
            $parentAssociation = $this->getParentAssociation($document);
2543
2544 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...
2545
                throw new \UnexpectedValueException('Could not determine parent association for ' . get_class($document));
2546
            }
2547
2548 33
            list(, $document, ) = $parentAssociation;
2549 33
            $class = $this->dm->getClassMetadata(get_class($document));
2550
        }
2551
2552 228
        return $document;
2553
    }
2554
2555
    /**
2556
     * Gets the class name for an association (embed or reference) with respect
2557
     * to any discriminator value.
2558
     *
2559
     * @param array      $mapping Field mapping for the association
2560
     * @param array|null $data    Data for the embedded document or reference
2561
     * @return string Class name.
2562
     */
2563 218
    public function getClassNameForAssociation(array $mapping, $data)
2564
    {
2565 218
        $discriminatorField = $mapping['discriminatorField'] ?? null;
2566
2567 218
        $discriminatorValue = null;
2568 218
        if (isset($discriminatorField, $data[$discriminatorField])) {
2569 21
            $discriminatorValue = $data[$discriminatorField];
2570 198
        } elseif (isset($mapping['defaultDiscriminatorValue'])) {
2571
            $discriminatorValue = $mapping['defaultDiscriminatorValue'];
2572
        }
2573
2574 218
        if ($discriminatorValue !== null) {
2575 21
            return $mapping['discriminatorMap'][$discriminatorValue]
2576 21
                ?? $discriminatorValue;
2577
        }
2578
2579 198
        $class = $this->dm->getClassMetadata($mapping['targetDocument']);
2580
2581 198
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2582 15
            $discriminatorValue = $data[$class->discriminatorField];
2583 184
        } elseif ($class->defaultDiscriminatorValue !== null) {
2584 1
            $discriminatorValue = $class->defaultDiscriminatorValue;
2585
        }
2586
2587 198
        if ($discriminatorValue !== null) {
2588 16
            return $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2589
        }
2590
2591 183
        return $mapping['targetDocument'];
2592
    }
2593
2594
    /**
2595
     * INTERNAL:
2596
     * Creates a document. Used for reconstitution of documents during hydration.
2597
     *
2598
     * @ignore
2599
     * @param string $className The name of the document class.
2600
     * @param array  $data      The data for the document.
2601
     * @param array  $hints     Any hints to account for during reconstitution/lookup of the document.
2602
     * @param object $document  The document to be hydrated into in case of creation
2603
     * @return object The document instance.
2604
     * @internal Highly performance-sensitive method.
2605
     */
2606 380
    public function getOrCreateDocument($className, $data, &$hints = [], $document = null)
2607
    {
2608 380
        $class = $this->dm->getClassMetadata($className);
2609
2610
        // @TODO figure out how to remove this
2611 380
        $discriminatorValue = null;
2612 380
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2613 17
            $discriminatorValue = $data[$class->discriminatorField];
2614 372
        } elseif (isset($class->defaultDiscriminatorValue)) {
2615 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2616
        }
2617
2618 380
        if ($discriminatorValue !== null) {
2619 18
            $className =  $class->discriminatorMap[$discriminatorValue] ?? $discriminatorValue;
2620
2621 18
            $class = $this->dm->getClassMetadata($className);
2622
2623 18
            unset($data[$class->discriminatorField]);
2624
        }
2625
2626 380
        if (! empty($hints[Query::HINT_READ_ONLY])) {
2627 2
            $document = $class->newInstance();
2628 2
            $this->hydratorFactory->hydrate($document, $data, $hints);
2629 2
            return $document;
2630
        }
2631
2632 379
        $isManagedObject = false;
2633 379
        $serializedId = null;
2634 379
        $id = null;
2635 379
        if (! $class->isQueryResultDocument) {
2636 376
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2637 376
            $serializedId = serialize($id);
2638 376
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2639
        }
2640
2641 379
        $oid = null;
2642 379
        if ($isManagedObject) {
2643 89
            $document = $this->identityMap[$class->name][$serializedId];
2644 89
            $oid = spl_object_hash($document);
2645 89
            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...
2646 14
                $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...
2647 14
                $overrideLocalValues = true;
2648 14
                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...
2649 14
                    $document->addPropertyChangedListener($this);
2650
                }
2651
            } else {
2652 81
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2653
            }
2654 89
            if ($overrideLocalValues) {
2655 38
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2656 89
                $this->originalDocumentData[$oid] = $data;
2657
            }
2658
        } else {
2659 342
            if ($document === null) {
2660 342
                $document = $class->newInstance();
2661
            }
2662
2663 342
            if (! $class->isQueryResultDocument) {
2664 338
                $this->registerManaged($document, $id, $data);
2665 338
                $oid = spl_object_hash($document);
2666 338
                $this->documentStates[$oid] = self::STATE_MANAGED;
2667 338
                $this->identityMap[$class->name][$serializedId] = $document;
2668
            }
2669
2670 342
            $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2671
2672 342
            if (! $class->isQueryResultDocument) {
2673 338
                $this->originalDocumentData[$oid] = $data;
2674
            }
2675
        }
2676
2677 379
        return $document;
2678
    }
2679
2680
    /**
2681
     * Initializes (loads) an uninitialized persistent collection of a document.
2682
     *
2683
     * @param PersistentCollectionInterface $collection The collection to initialize.
2684
     */
2685 164
    public function loadCollection(PersistentCollectionInterface $collection)
2686
    {
2687 164
        $this->getDocumentPersister(get_class($collection->getOwner()))->loadCollection($collection);
2688 164
        $this->lifecycleEventManager->postCollectionLoad($collection);
2689 164
    }
2690
2691
    /**
2692
     * Gets the identity map of the UnitOfWork.
2693
     *
2694
     * @return array
2695
     */
2696
    public function getIdentityMap()
2697
    {
2698
        return $this->identityMap;
2699
    }
2700
2701
    /**
2702
     * Gets the original data of a document. The original data is the data that was
2703
     * present at the time the document was reconstituted from the database.
2704
     *
2705
     * @param object $document
2706
     * @return array
2707
     */
2708 1
    public function getOriginalDocumentData($document)
2709
    {
2710 1
        $oid = spl_object_hash($document);
2711
2712 1
        return $this->originalDocumentData[$oid] ?? [];
2713
    }
2714
2715 59
    public function setOriginalDocumentData($document, array $data)
2716
    {
2717 59
        $oid = spl_object_hash($document);
2718 59
        $this->originalDocumentData[$oid] = $data;
2719 59
        unset($this->documentChangeSets[$oid]);
2720 59
    }
2721
2722
    /**
2723
     * INTERNAL:
2724
     * Sets a property value of the original data array of a document.
2725
     *
2726
     * @ignore
2727
     * @param string $oid
2728
     * @param string $property
2729
     * @param mixed  $value
2730
     */
2731 3
    public function setOriginalDocumentProperty($oid, $property, $value)
2732
    {
2733 3
        $this->originalDocumentData[$oid][$property] = $value;
2734 3
    }
2735
2736
    /**
2737
     * Gets the identifier of a document.
2738
     *
2739
     * @param object $document
2740
     * @return mixed The identifier value
2741
     */
2742 411
    public function getDocumentIdentifier($document)
2743
    {
2744 411
        return $this->documentIdentifiers[spl_object_hash($document)] ?? null;
2745
    }
2746
2747
    /**
2748
     * Checks whether the UnitOfWork has any pending insertions.
2749
     *
2750
     * @return bool TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
2751
     */
2752
    public function hasPendingInsertions()
2753
    {
2754
        return ! empty($this->documentInsertions);
2755
    }
2756
2757
    /**
2758
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
2759
     * number of documents in the identity map.
2760
     *
2761
     * @return int
2762
     */
2763 2
    public function size()
2764
    {
2765 2
        $count = 0;
2766 2
        foreach ($this->identityMap as $documentSet) {
2767 2
            $count += count($documentSet);
2768
        }
2769 2
        return $count;
2770
    }
2771
2772
    /**
2773
     * INTERNAL:
2774
     * Registers a document as managed.
2775
     *
2776
     * TODO: This method assumes that $id is a valid PHP identifier for the
2777
     * document class. If the class expects its database identifier to be an
2778
     * ObjectId, and an incompatible $id is registered (e.g. an integer), the
2779
     * document identifiers map will become inconsistent with the identity map.
2780
     * In the future, we may want to round-trip $id through a PHP and database
2781
     * conversion and throw an exception if it's inconsistent.
2782
     *
2783
     * @param object $document The document.
2784
     * @param array  $id       The identifier values.
2785
     * @param array  $data     The original document data.
2786
     */
2787 363
    public function registerManaged($document, $id, $data)
2788
    {
2789 363
        $oid = spl_object_hash($document);
2790 363
        $class = $this->dm->getClassMetadata(get_class($document));
2791
2792 363
        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...
2793 92
            $this->documentIdentifiers[$oid] = $oid;
2794
        } else {
2795 357
            $this->documentIdentifiers[$oid] = $class->getPHPIdentifierValue($id);
2796
        }
2797
2798 363
        $this->documentStates[$oid] = self::STATE_MANAGED;
2799 363
        $this->originalDocumentData[$oid] = $data;
2800 363
        $this->addToIdentityMap($document);
2801 363
    }
2802
2803
    /**
2804
     * INTERNAL:
2805
     * Clears the property changeset of the document with the given OID.
2806
     *
2807
     * @param string $oid The document's OID.
2808
     */
2809
    public function clearDocumentChangeSet($oid)
2810
    {
2811
        $this->documentChangeSets[$oid] = [];
2812
    }
2813
2814
    /* PropertyChangedListener implementation */
2815
2816
    /**
2817
     * Notifies this UnitOfWork of a property change in a document.
2818
     *
2819
     * @param object $document     The document that owns the property.
2820
     * @param string $propertyName The name of the property that changed.
2821
     * @param mixed  $oldValue     The old value of the property.
2822
     * @param mixed  $newValue     The new value of the property.
2823
     */
2824 2
    public function propertyChanged($document, $propertyName, $oldValue, $newValue)
2825
    {
2826 2
        $oid = spl_object_hash($document);
2827 2
        $class = $this->dm->getClassMetadata(get_class($document));
2828
2829 2
        if (! isset($class->fieldMappings[$propertyName])) {
2830 1
            return; // ignore non-persistent fields
2831
        }
2832
2833
        // Update changeset and mark document for synchronization
2834 2
        $this->documentChangeSets[$oid][$propertyName] = [$oldValue, $newValue];
2835 2
        if (isset($this->scheduledForDirtyCheck[$class->name][$oid])) {
2836
            return;
2837
        }
2838
2839 2
        $this->scheduleForDirtyCheck($document);
2840 2
    }
2841
2842
    /**
2843
     * Gets the currently scheduled document insertions in this UnitOfWork.
2844
     *
2845
     * @return array
2846
     */
2847 2
    public function getScheduledDocumentInsertions()
2848
    {
2849 2
        return $this->documentInsertions;
2850
    }
2851
2852
    /**
2853
     * Gets the currently scheduled document upserts in this UnitOfWork.
2854
     *
2855
     * @return array
2856
     */
2857 1
    public function getScheduledDocumentUpserts()
2858
    {
2859 1
        return $this->documentUpserts;
2860
    }
2861
2862
    /**
2863
     * Gets the currently scheduled document updates in this UnitOfWork.
2864
     *
2865
     * @return array
2866
     */
2867 1
    public function getScheduledDocumentUpdates()
2868
    {
2869 1
        return $this->documentUpdates;
2870
    }
2871
2872
    /**
2873
     * Gets the currently scheduled document deletions in this UnitOfWork.
2874
     *
2875
     * @return array
2876
     */
2877
    public function getScheduledDocumentDeletions()
2878
    {
2879
        return $this->documentDeletions;
2880
    }
2881
2882
    /**
2883
     * Get the currently scheduled complete collection deletions
2884
     *
2885
     * @return array
2886
     */
2887
    public function getScheduledCollectionDeletions()
2888
    {
2889
        return $this->collectionDeletions;
2890
    }
2891
2892
    /**
2893
     * Gets the currently scheduled collection inserts, updates and deletes.
2894
     *
2895
     * @return array
2896
     */
2897
    public function getScheduledCollectionUpdates()
2898
    {
2899
        return $this->collectionUpdates;
2900
    }
2901
2902
    /**
2903
     * Helper method to initialize a lazy loading proxy or persistent collection.
2904
     *
2905
     * @param object $obj
2906
     */
2907
    public function initializeObject($obj)
2908
    {
2909
        if ($obj instanceof Proxy) {
2910
            $obj->__load();
2911
        } elseif ($obj instanceof PersistentCollectionInterface) {
2912
            $obj->initialize();
2913
        }
2914
    }
2915
2916
    private function objToStr($obj)
2917
    {
2918
        return method_exists($obj, '__toString') ? (string) $obj : get_class($obj) . '@' . spl_object_hash($obj);
2919
    }
2920
}
2921