Completed
Pull Request — master (#1787)
by Stefano
21:31
created

UnitOfWork::getDocumentActualData()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 19
cts 19
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 5
nop 1
crap 9
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 1584
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
266
    {
267 1584
        $this->dm = $dm;
268 1584
        $this->evm = $evm;
269 1584
        $this->hydratorFactory = $hydratorFactory;
270 1584
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
271 1584
    }
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 1080
    public function getPersistenceBuilder()
280
    {
281 1080
        if (! $this->persistenceBuilder) {
282 1080
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
283
        }
284 1080
        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 178
    public function setParentAssociation($document, $mapping, $parent, $propertyPath)
296
    {
297 178
        $oid = spl_object_hash($document);
298 178
        $this->embeddedDocumentsRegistry[$oid] = $document;
299 178
        $this->parentAssociations[$oid] = [$mapping, $parent, $propertyPath];
300 178
    }
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 205
    public function getParentAssociation($document)
313
    {
314 205
        $oid = spl_object_hash($document);
315
316 205
        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 1078
    public function getDocumentPersister($documentName)
326
    {
327 1078
        if (! isset($this->persisters[$documentName])) {
328 1065
            $class = $this->dm->getClassMetadata($documentName);
329 1065
            $pb = $this->getPersistenceBuilder();
330 1065
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
331
        }
332 1078
        return $this->persisters[$documentName];
333
    }
334
335
    /**
336
     * Get the collection persister instance.
337
     *
338
     * @return CollectionPersister
339
     */
340 1078
    public function getCollectionPersister()
341
    {
342 1078
        if (! isset($this->collectionPersister)) {
343 1078
            $pb = $this->getPersistenceBuilder();
344 1078
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
345
        }
346 1078
        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 569
    public function commit(array $options = [])
373
    {
374
        // Raise preFlush
375 569
        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 569
        $this->computeChangeSets();
381
382 568
        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 242
            $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 201
            $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 568
            $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 565
        $this->commitsInProgress++;
394 565
        if ($this->commitsInProgress > 1) {
395
            throw MongoDBException::commitInProgress();
396
        }
397
        try {
398 565
            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 565
            if ($this->evm->hasListeners(Events::onFlush)) {
406 4
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
407
            }
408
409 564
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
410 84
                list($class, $documents) = $classAndDocuments;
411 84
                $this->executeUpserts($class, $documents, $options);
412
            }
413
414 564
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
415 491
                list($class, $documents) = $classAndDocuments;
416 491
                $this->executeInserts($class, $documents, $options);
417
            }
418
419 563
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
420 205
                list($class, $documents) = $classAndDocuments;
421 205
                $this->executeUpdates($class, $documents, $options);
422
            }
423
424 563
            foreach ($this->getClassesForCommitAction($this->documentDeletions, true) as $classAndDocuments) {
425 66
                list($class, $documents) = $classAndDocuments;
426 66
                $this->executeDeletions($class, $documents, $options);
427
            }
428
429
            // Raise postFlush
430 563
            if ($this->evm->hasListeners(Events::postFlush)) {
431
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
432
            }
433
434
            // Clear up
435 563
            $this->documentInsertions =
436 563
            $this->documentUpserts =
437 563
            $this->documentUpdates =
438 563
            $this->documentDeletions =
439 563
            $this->documentChangeSets =
440 563
            $this->collectionUpdates =
441 563
            $this->collectionDeletions =
442 563
            $this->visitedCollections =
443 563
            $this->scheduledForDirtyCheck =
444 563
            $this->orphanRemovals =
445 563
            $this->hasScheduledCollections = [];
446 563
        } finally {
447 565
            $this->commitsInProgress--;
448
        }
449 563
    }
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 564
    private function getClassesForCommitAction($documents, $includeEmbedded = false)
459
    {
460 564
        if (empty($documents)) {
461 564
            return [];
462
        }
463 563
        $divided = [];
464 563
        $embeds = [];
465 563
        foreach ($documents as $oid => $d) {
466 563
            $className = get_class($d);
467 563
            if (isset($embeds[$className])) {
468 69
                continue;
469
            }
470 563
            if (isset($divided[$className])) {
471 156
                $divided[$className][1][$oid] = $d;
472 156
                continue;
473
            }
474 563
            $class = $this->dm->getClassMetadata($className);
475 563
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
476 155
                $embeds[$className] = true;
477 155
                continue;
478
            }
479 563
            if (empty($divided[$class->name])) {
480 563
                $divided[$class->name] = [$class, [$oid => $d]];
481
            } else {
482 563
                $divided[$class->name][1][$oid] = $d;
483
            }
484
        }
485 563
        return $divided;
486
    }
487
488
    /**
489
     * Compute changesets of all documents scheduled for insertion.
490
     *
491
     * Embedded documents will not be processed.
492
     */
493 572
    private function computeScheduleInsertsChangeSets()
494
    {
495 572
        foreach ($this->documentInsertions as $document) {
496 502
            $class = $this->dm->getClassMetadata(get_class($document));
497 502
            if ($class->isEmbeddedDocument) {
498 139
                continue;
499
            }
500
501 497
            $this->computeChangeSet($class, $document);
502
        }
503 571
    }
504
505
    /**
506
     * Compute changesets of all documents scheduled for upsert.
507
     *
508
     * Embedded documents will not be processed.
509
     */
510 571
    private function computeScheduleUpsertsChangeSets()
511
    {
512 571
        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 571
    }
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 566
    public function getDocumentChangeSet($document)
529
    {
530 566
        $oid = spl_object_hash($document);
531
532 566
        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 573
    public function getDocumentActualData($document)
554
    {
555 573
        $class = $this->dm->getClassMetadata(get_class($document));
556 573
        $actualData = [];
557 573
        foreach ($class->reflFields as $name => $refProp) {
558 573
            $mapping = $class->fieldMappings[$name];
559
            // skip not saved fields
560 573
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
561 27
                continue;
562
            }
563 573
            $value = $refProp->getValue($document);
564 573
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
565 573
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
566
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
567 369
                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 143
                    $value = new ArrayCollection($value);
569
                }
570
571
                // Inject PersistentCollection
572 369
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
573 369
                $coll->setOwner($document, $mapping);
574 369
                $coll->setDirty(! $value->isEmpty());
575 369
                $class->reflFields[$name]->setValue($document, $coll);
576 369
                $actualData[$name] = $coll;
577
            } else {
578 573
                $actualData[$name] = $value;
579
            }
580
        }
581 573
        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 569
    public function computeChangeSet(ClassMetadata $class, $document)
609
    {
610 569
        if (! $class->isInheritanceTypeNone()) {
611 175
            $class = $this->dm->getClassMetadata(get_class($document));
612
        }
613
614
        // Fire PreFlush lifecycle callbacks
615 569
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
616 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
617
        }
618
619 569
        $this->computeOrRecomputeChangeSet($class, $document);
620 568
    }
621
622
    /**
623
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
624
     *
625
     * @param object $document
626
     * @param bool   $recompute
627
     */
628 569
    private function computeOrRecomputeChangeSet(ClassMetadata $class, $document, $recompute = false)
629
    {
630 569
        $oid = spl_object_hash($document);
631 569
        $actualData = $this->getDocumentActualData($document);
632 569
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
633 569
        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 569
            $this->originalDocumentData[$oid] = $actualData;
637 569
            $changeSet = [];
638 569
            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 569
                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 569
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
648 180
                    continue;
649
                }
650 569
                $changeSet[$propName] = [null, $actualValue];
651
            }
652 569
            $this->documentChangeSets[$oid] = $changeSet;
653
        } else {
654 260
            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 258
            $originalData = $this->originalDocumentData[$oid];
660 258
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
661 258
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
662 2
                $changeSet = $this->documentChangeSets[$oid];
663
            } else {
664 258
                $changeSet = [];
665
            }
666
667 258
            foreach ($actualData as $propName => $actualValue) {
668
                // skip not saved fields
669 258
                if (isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) {
670
                    continue;
671
                }
672
673 258
                $orgValue = $originalData[$propName] ?? null;
674
675
                // skip if value has not changed
676 258
                if ($orgValue === $actualValue) {
677 257
                    if (! $actualValue instanceof PersistentCollectionInterface) {
678 257
                        continue;
679
                    }
680
681 178
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
682
                        // consider dirty collections as changed as well
683 154
                        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 258
            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 569
        $associationMappings = array_filter(
767 569
            $class->associationMappings,
768
            function ($assoc) {
769 432
                return empty($assoc['notSaved']);
770 569
            }
771
        );
772
773 569
        foreach ($associationMappings as $mapping) {
774 432
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
775
776 432
            if ($value === null) {
777 295
                continue;
778
            }
779
780 421
            $this->computeAssociationChanges($document, $mapping, $value);
781
782 420
            if (isset($mapping['reference'])) {
783 317
                continue;
784
            }
785
786 322
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
787
788 322
            foreach ($values as $obj) {
789 159
                $oid2 = spl_object_hash($obj);
790
791 159
                if (isset($this->documentChangeSets[$oid2])) {
792 157
                    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 157
                    if (! $isNewDocument) {
799 65
                        $this->scheduleForUpdate($document);
800
                    }
801
802 322
                    break;
803
                }
804
            }
805
        }
806 568
    }
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 572
    public function computeChangeSets()
814
    {
815 572
        $this->computeScheduleInsertsChangeSets();
816 571
        $this->computeScheduleUpsertsChangeSets();
817
818
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
819 571
        foreach ($this->identityMap as $className => $documents) {
820 571
            $class = $this->dm->getClassMetadata($className);
821 571
            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 151
                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 571
                case ($class->isChangeTrackingDeferredImplicit()):
834 570
                    $documentsToProcess = $documents;
835 570
                    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 571
            foreach ($documentsToProcess as $document) {
846
                // Ignore uninitialized proxy objects
847 567
                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 567
                $oid = spl_object_hash($document);
852 567
                if (isset($this->documentInsertions[$oid])
853 300
                    || isset($this->documentUpserts[$oid])
854 256
                    || isset($this->documentDeletions[$oid])
855 567
                    || ! isset($this->documentStates[$oid])
856
                ) {
857 567
                    continue;
858
                }
859
860 571
                $this->computeChangeSet($class, $document);
861
            }
862
        }
863 571
    }
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 421
    private function computeAssociationChanges($parentDocument, array $assoc, $value)
874
    {
875 421
        $isNewParentDocument = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
876 421
        $class = $this->dm->getClassMetadata(get_class($parentDocument));
877 421
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
878
879 421
        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 420
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
884 230
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
885 226
                $this->scheduleCollectionUpdate($value);
886
            }
887 230
            $topmostOwner = $this->getOwningDocument($value->getOwner());
888 230
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
889 230
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
890 124
                $value->initialize();
891 124
                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 420
        $unwrappedValue = ($assoc['type'] === ClassMetadata::ONE) ? [$value] : $value->unwrap();
901
902 420
        $count = 0;
903 420
        foreach ($unwrappedValue as $key => $entry) {
904 338
            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 337
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
911
912 337
            $state = $this->getDocumentState($entry, self::STATE_NEW);
913
914
            // Handle "set" strategy for multi-level hierarchy
915 337
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
916 337
            $path = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
917
918 337
            $count++;
919
920
            switch ($state) {
921 337
                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 333
                case self::STATE_MANAGED:
935 333
                    if ($targetClass->isEmbeddedDocument) {
936 151
                        list(, $knownParent, ) = $this->getParentAssociation($entry);
937 151
                        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 151
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
953 151
                        $this->computeChangeSet($targetClass, $entry);
954
                    }
955 333
                    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 337
                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 419
    }
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 599
    private function persistNew(ClassMetadata $class, $document)
1017
    {
1018 599
        $this->lifecycleEventManager->prePersist($class, $document);
1019 599
        $oid = spl_object_hash($document);
1020 599
        $upsert = false;
1021 599
        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 599
            $idValue = $class->getIdentifierValue($document);
1023 599
            $upsert = ! $class->isEmbeddedDocument && $idValue !== null;
1024
1025 599
            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 598
            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 597
            if ($class->generatorType !== ClassMetadata::GENERATOR_TYPE_NONE && $idValue === null) {
1040 520
                $idValue = $class->idGenerator->generate($this->dm, $document);
1041 520
                $idValue = $class->getPHPIdentifierValue($class->getDatabaseIdentifierValue($idValue));
1042 520
                $class->setIdentifierValue($document, $idValue);
1043
            }
1044
1045 597
            $this->documentIdentifiers[$oid] = $idValue;
1046
        } else {
1047
            // this is for embedded documents without identifiers
1048 131
            $this->documentIdentifiers[$oid] = $oid;
1049
        }
1050
1051 597
        $this->documentStates[$oid] = self::STATE_MANAGED;
1052
1053 597
        if ($upsert) {
1054 87
            $this->scheduleForUpsert($class, $document);
1055
        } else {
1056 528
            $this->scheduleForInsert($class, $document);
1057
        }
1058 597
    }
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 491
    private function executeInserts(ClassMetadata $class, array $documents, array $options = [])
1067
    {
1068 491
        $persister = $this->getDocumentPersister($class->name);
1069
1070 491
        foreach ($documents as $oid => $document) {
1071 491
            $persister->addInsert($document);
1072 491
            unset($this->documentInsertions[$oid]);
1073
        }
1074
1075 491
        $persister->executeInserts($options);
1076
1077 490
        foreach ($documents as $document) {
1078 490
            $this->lifecycleEventManager->postPersist($class, $document);
1079
        }
1080 490
    }
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 66
    private function executeDeletions(ClassMetadata $class, array $documents, array $options = [])
1139
    {
1140 66
        $persister = $this->getDocumentPersister($class->name);
1141
1142 66
        foreach ($documents as $oid => $document) {
1143 66
            if (! $class->isEmbeddedDocument) {
1144 34
                $persister->delete($document, $options);
1145
            }
1146
            unset(
1147 64
                $this->documentDeletions[$oid],
1148 64
                $this->documentIdentifiers[$oid],
1149 64
                $this->originalDocumentData[$oid]
1150
            );
1151
1152
            // Clear snapshot information for any referenced PersistentCollection
1153
            // http://www.doctrine-project.org/jira/browse/MODM-95
1154 64
            foreach ($class->associationMappings as $fieldMapping) {
1155 40
                if (! isset($fieldMapping['type']) || $fieldMapping['type'] !== ClassMetadata::MANY) {
1156 30
                    continue;
1157
                }
1158
1159 26
                $value = $class->reflFields[$fieldMapping['fieldName']]->getValue($document);
1160 26
                if (! ($value instanceof PersistentCollectionInterface)) {
1161 7
                    continue;
1162
                }
1163
1164 22
                $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 64
            $this->documentStates[$oid] = self::STATE_NEW;
1170
1171 64
            $this->lifecycleEventManager->postRemove($class, $document);
1172
        }
1173 64
    }
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 531
    public function scheduleForInsert(ClassMetadata $class, $document)
1184
    {
1185 531
        $oid = spl_object_hash($document);
1186
1187 531
        if (isset($this->documentUpdates[$oid])) {
1188
            throw new \InvalidArgumentException('Dirty document can not be scheduled for insertion.');
1189
        }
1190 531
        if (isset($this->documentDeletions[$oid])) {
1191
            throw new \InvalidArgumentException('Removed document can not be scheduled for insertion.');
1192
        }
1193 531
        if (isset($this->documentInsertions[$oid])) {
1194
            throw new \InvalidArgumentException('Document can not be scheduled for insertion twice.');
1195
        }
1196
1197 531
        $this->documentInsertions[$oid] = $document;
1198
1199 531
        if (! isset($this->documentIdentifiers[$oid])) {
1200 3
            return;
1201
        }
1202
1203 528
        $this->addToIdentityMap($document);
1204 528
    }
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 71
    public function scheduleForDelete($document)
1309
    {
1310 71
        $oid = spl_object_hash($document);
1311
1312 71
        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 70
        if (! $this->isInIdentityMap($document)) {
1321 2
            return; // ignore
1322
        }
1323
1324 69
        $this->removeFromIdentityMap($document);
1325 69
        $this->documentStates[$oid] = self::STATE_REMOVED;
1326
1327 69
        if (isset($this->documentUpdates[$oid])) {
1328
            unset($this->documentUpdates[$oid]);
1329
        }
1330 69
        if (isset($this->documentDeletions[$oid])) {
1331
            return;
1332
        }
1333
1334 69
        $this->documentDeletions[$oid] = $document;
1335 69
    }
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 229
    public function isDocumentScheduled($document)
1356
    {
1357 229
        $oid = spl_object_hash($document);
1358 229
        return isset($this->documentInsertions[$oid]) ||
1359 113
            isset($this->documentUpserts[$oid]) ||
1360 104
            isset($this->documentUpdates[$oid]) ||
1361 229
            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 629
    public function addToIdentityMap($document)
1378
    {
1379 629
        $class = $this->dm->getClassMetadata(get_class($document));
1380 629
        $id = $this->getIdForIdentityMap($document);
1381
1382 629
        if (isset($this->identityMap[$class->name][$id])) {
1383 42
            return false;
1384
        }
1385
1386 629
        $this->identityMap[$class->name][$id] = $document;
1387
1388 629
        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 629
            ( ! $document instanceof Proxy || $document->__isInitialized())) {
1390 3
            $document->addPropertyChangedListener($this);
1391
        }
1392
1393 629
        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 601
    public function getDocumentState($document, $assume = null)
1407
    {
1408 601
        $oid = spl_object_hash($document);
1409
1410 601
        if (isset($this->documentStates[$oid])) {
1411 370
            return $this->documentStates[$oid];
1412
        }
1413
1414 601
        $class = $this->dm->getClassMetadata(get_class($document));
1415
1416 601
        if ($class->isEmbeddedDocument) {
1417 164
            return self::STATE_NEW;
1418
        }
1419
1420 598
        if ($assume !== null) {
1421 596
            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 83
    public function removeFromIdentityMap($document)
1468
    {
1469 83
        $oid = spl_object_hash($document);
1470
1471
        // Check if id is registered first
1472 83
        if (! isset($this->documentIdentifiers[$oid])) {
1473
            return false;
1474
        }
1475
1476 83
        $class = $this->dm->getClassMetadata(get_class($document));
1477 83
        $id = $this->getIdForIdentityMap($document);
1478
1479 83
        if (isset($this->identityMap[$class->name][$id])) {
1480 83
            unset($this->identityMap[$class->name][$id]);
1481 83
            $this->documentStates[$oid] = self::STATE_DETACHED;
1482 83
            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 296
    public function tryGetById($id, ClassMetadata $class)
1521
    {
1522 296
        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 296
        $serializedId = serialize($class->getDatabaseIdentifierValue($id));
1527
1528 296
        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 79
    public function isInIdentityMap($document)
1550
    {
1551 79
        $oid = spl_object_hash($document);
1552
1553 79
        if (! isset($this->documentIdentifiers[$oid])) {
1554 6
            return false;
1555
        }
1556
1557 77
        $class = $this->dm->getClassMetadata(get_class($document));
1558 77
        $id = $this->getIdForIdentityMap($document);
1559
1560 77
        return isset($this->identityMap[$class->name][$id]);
1561
    }
1562
1563
    /**
1564
     * @param object $document
1565
     * @return string
1566
     */
1567 629
    private function getIdForIdentityMap($document)
1568
    {
1569 629
        $class = $this->dm->getClassMetadata(get_class($document));
1570
1571 629
        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 134
            $id = spl_object_hash($document);
1573
        } else {
1574 628
            $id = $this->documentIdentifiers[spl_object_hash($document)];
1575 628
            $id = serialize($class->getDatabaseIdentifierValue($id));
1576
        }
1577
1578 629
        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 599
    public function persist($document)
1603
    {
1604 599
        $class = $this->dm->getClassMetadata(get_class($document));
1605 599
        if ($class->isMappedSuperclass || $class->isQueryResultDocument) {
1606 1
            throw MongoDBException::cannotPersistMappedSuperclass($class->name);
1607
        }
1608 598
        $visited = [];
1609 598
        $this->doPersist($document, $visited);
1610 594
    }
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 598
    private function doPersist($document, array &$visited)
1626
    {
1627 598
        $oid = spl_object_hash($document);
1628 598
        if (isset($visited[$oid])) {
1629 25
            return; // Prevent infinite recursion
1630
        }
1631
1632 598
        $visited[$oid] = $document; // Mark visited
1633
1634 598
        $class = $this->dm->getClassMetadata(get_class($document));
1635
1636 598
        $documentState = $this->getDocumentState($document, self::STATE_NEW);
1637
        switch ($documentState) {
1638 598
            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 598
            case self::STATE_NEW:
1645 598
                $this->persistNew($class, $document);
1646 596
                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 596
        $this->cascadePersist($document, $visited);
1665 594
    }
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 70
    public function remove($document)
1673
    {
1674 70
        $visited = [];
1675 70
        $this->doRemove($document, $visited);
1676 70
    }
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 70
    private function doRemove($document, array &$visited)
1689
    {
1690 70
        $oid = spl_object_hash($document);
1691 70
        if (isset($visited[$oid])) {
1692 1
            return; // Prevent infinite recursion
1693
        }
1694
1695 70
        $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 70
        $this->cascadeRemove($document, $visited);
1702
1703 70
        $class = $this->dm->getClassMetadata(get_class($document));
1704 70
        $documentState = $this->getDocumentState($document);
1705
        switch ($documentState) {
1706 70
            case self::STATE_NEW:
1707 70
            case self::STATE_REMOVED:
1708
                // nothing to do
1709
                break;
1710 70
            case self::STATE_MANAGED:
1711 70
                $this->lifecycleEventManager->preRemove($class, $document);
1712 70
                $this->scheduleForDelete($document);
1713 70
                break;
1714
            case self::STATE_DETACHED:
1715
                throw MongoDBException::detachedDocumentCannotBeRemoved();
1716
            default:
1717
                throw MongoDBException::invalidDocumentState($documentState);
1718
        }
1719 70
    }
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 ClassMetadata $targetClass */
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 596
    private function cascadePersist($document, array &$visited)
2123
    {
2124 596
        $class = $this->dm->getClassMetadata(get_class($document));
2125
2126 596
        $associationMappings = array_filter(
2127 596
            $class->associationMappings,
2128
            function ($assoc) {
2129 454
                return $assoc['isCascadePersist'];
2130 596
            }
2131
        );
2132
2133 596
        foreach ($associationMappings as $fieldName => $mapping) {
2134 410
            $relatedDocuments = $class->reflFields[$fieldName]->getValue($document);
2135
2136 410
            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 339
                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 339
                $count = 0;
2146 339
                foreach ($relatedDocuments as $relatedKey => $relatedDocument) {
2147 175
                    if (! empty($mapping['embedded'])) {
2148 104
                        list(, $knownParent, ) = $this->getParentAssociation($relatedDocument);
2149 104
                        if ($knownParent && $knownParent !== $document) {
2150 1
                            $relatedDocument = clone $relatedDocument;
2151 1
                            $relatedDocuments[$relatedKey] = $relatedDocument;
2152
                        }
2153 104
                        $pathKey = CollectionHelper::isList($mapping['strategy']) ? $count++ : $relatedKey;
2154 104
                        $this->setParentAssociation($relatedDocument, $mapping, $document, $mapping['fieldName'] . '.' . $pathKey);
2155
                    }
2156 339
                    $this->doPersist($relatedDocument, $visited);
2157
                }
2158 325
            } 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 410
                $this->doPersist($relatedDocuments, $visited);
2168
            }
2169
        }
2170 594
    }
2171
2172
    /**
2173
     * Cascades the delete operation to associated documents.
2174
     *
2175
     * @param object $document
2176
     * @param array  $visited
2177
     */
2178 70
    private function cascadeRemove($document, array &$visited)
2179
    {
2180 70
        $class = $this->dm->getClassMetadata(get_class($document));
2181 70
        foreach ($class->fieldMappings as $mapping) {
2182 70
            if (! $mapping['isCascadeRemove'] && ( ! isset($mapping['orphanRemoval']) || ! $mapping['orphanRemoval'])) {
2183 69
                continue;
2184
            }
2185 35
            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 35
            $relatedDocuments = $class->reflFields[$mapping['fieldName']]->getValue($document);
2190 35
            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 24
                foreach ($relatedDocuments as $relatedDocument) {
2193 24
                    $this->doRemove($relatedDocument, $visited);
2194
                }
2195 24
            } elseif ($relatedDocuments !== null) {
2196 35
                $this->doRemove($relatedDocuments, $visited);
2197
            }
2198
        }
2199 70
    }
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 192
    public function isCollectionScheduledForDeletion(PersistentCollectionInterface $coll)
2376
    {
2377 192
        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 205
    public function unscheduleCollectionDeletion(PersistentCollectionInterface $coll)
2386
    {
2387 205
        $oid = spl_object_hash($coll);
2388 205
        if (! isset($this->collectionDeletions[$oid])) {
2389 205
            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 226
    public function scheduleCollectionUpdate(PersistentCollectionInterface $coll)
2403
    {
2404 226
        $mapping = $coll->getMapping();
2405 226
        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 226
        $oid = spl_object_hash($coll);
2412 226
        if (isset($this->collectionUpdates[$oid])) {
2413 7
            return;
2414
        }
2415
2416 226
        $this->collectionUpdates[$oid] = $coll;
2417 226
        $this->scheduleCollectionOwner($coll);
2418 226
    }
2419
2420
    /**
2421
     * INTERNAL:
2422
     * Unschedules a collection from being updated when this UnitOfWork commits.
2423
     *
2424
     */
2425 205
    public function unscheduleCollectionUpdate(PersistentCollectionInterface $coll)
2426
    {
2427 205
        $oid = spl_object_hash($coll);
2428 205
        if (! isset($this->collectionUpdates[$oid])) {
2429 35
            return;
2430
        }
2431
2432 195
        $topmostOwner = $this->getOwningDocument($coll->getOwner());
2433 195
        unset($this->collectionUpdates[$oid]);
2434 195
        unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]);
2435 195
    }
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 550
    public function getVisitedCollections($document)
2456
    {
2457 550
        $oid = spl_object_hash($document);
2458
2459 550
        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 550
    public function getScheduledCollections($document)
2470
    {
2471 550
        $oid = spl_object_hash($document);
2472
2473 550
        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 228
    private function scheduleCollectionOwner(PersistentCollectionInterface $coll)
2501
    {
2502 228
        $document = $this->getOwningDocument($coll->getOwner());
2503 228
        $this->hasScheduledCollections[spl_object_hash($document)][spl_object_hash($coll)] = $coll;
2504
2505 228
        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 228
        if ($this->isDocumentScheduled($document)) {
2521 223
            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 230
    public function getOwningDocument($document)
2539
    {
2540 230
        $class = $this->dm->getClassMetadata(get_class($document));
2541 230
        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 230
        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 381
    public function getOrCreateDocument($className, $data, &$hints = [], $document = null)
2607
    {
2608 381
        $class = $this->dm->getClassMetadata($className);
2609
2610
        // @TODO figure out how to remove this
2611 381
        $discriminatorValue = null;
2612 381
        if (isset($class->discriminatorField, $data[$class->discriminatorField])) {
2613 17
            $discriminatorValue = $data[$class->discriminatorField];
2614 373
        } elseif (isset($class->defaultDiscriminatorValue)) {
2615 2
            $discriminatorValue = $class->defaultDiscriminatorValue;
2616
        }
2617
2618 381
        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 381
        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 380
        $isManagedObject = false;
2633 380
        $serializedId = null;
2634 380
        $id = null;
2635 380
        if (! $class->isQueryResultDocument) {
2636 377
            $id = $class->getDatabaseIdentifierValue($data['_id']);
2637 377
            $serializedId = serialize($id);
2638 377
            $isManagedObject = isset($this->identityMap[$class->name][$serializedId]);
2639
        }
2640
2641 380
        $oid = null;
2642 380
        if ($isManagedObject) {
2643 90
            $document = $this->identityMap[$class->name][$serializedId];
2644 90
            $oid = spl_object_hash($document);
2645 90
            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 82
                $overrideLocalValues = ! empty($hints[Query::HINT_REFRESH]);
2653
            }
2654 90
            if ($overrideLocalValues) {
2655 38
                $data = $this->hydratorFactory->hydrate($document, $data, $hints);
2656 90
                $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 380
        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 414
    public function getDocumentIdentifier($document)
2743
    {
2744 414
        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