Completed
Push — master ( 26ecbc...8c0c5d )
by Maciej
14s
created

UnitOfWork::scheduleCollectionOwner()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 19
cts 20
cp 0.95
rs 8.8177
c 0
b 0
f 0
cc 6
nc 7
nop 1
crap 6.0045
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\Common\Collections\ArrayCollection;
8
use Doctrine\Common\Collections\Collection;
9
use Doctrine\Common\EventManager;
10
use Doctrine\Common\NotifyPropertyChanged;
11
use Doctrine\Common\PropertyChangedListener;
12
use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory;
13
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
14
use Doctrine\ODM\MongoDB\Mapping\MappingException;
15
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionException;
16
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
17
use Doctrine\ODM\MongoDB\Persisters\CollectionPersister;
18
use Doctrine\ODM\MongoDB\Persisters\PersistenceBuilder;
19
use Doctrine\ODM\MongoDB\Query\Query;
20
use Doctrine\ODM\MongoDB\Types\DateType;
21
use Doctrine\ODM\MongoDB\Types\Type;
22
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
23
use Doctrine\ODM\MongoDB\Utility\LifecycleEventManager;
24
use InvalidArgumentException;
25
use MongoDB\BSON\UTCDateTime;
26
use ProxyManager\Proxy\GhostObjectInterface;
27
use UnexpectedValueException;
28
use function array_filter;
29
use function count;
30
use function get_class;
31
use function in_array;
32
use function is_array;
33
use function is_object;
34
use function method_exists;
35
use function preg_match;
36
use function serialize;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * The UnitOfWork is responsible for tracking changes to objects during an
42
 * "object-level" transaction and for writing out changes to the database
43
 * in the correct order.
44
 */
45
class UnitOfWork implements PropertyChangedListener
46
{
47
    /**
48
     * A document is in MANAGED state when its persistence is managed by a DocumentManager.
49
     */
50
    public const STATE_MANAGED = 1;
51
52
    /**
53
     * A document is new if it has just been instantiated (i.e. using the "new" operator)
54
     * and is not (yet) managed by a DocumentManager.
55
     */
56
    public const STATE_NEW = 2;
57
58
    /**
59
     * A detached document is an instance with a persistent identity that is not
60
     * (or no longer) associated with a DocumentManager (and a UnitOfWork).
61
     */
62
    public const STATE_DETACHED = 3;
63
64
    /**
65
     * A removed document instance is an instance with a persistent identity,
66
     * associated with a DocumentManager, whose persistent state has been
67
     * deleted (or is scheduled for deletion).
68
     */
69
    public const STATE_REMOVED = 4;
70
71
    /**
72
     * The identity map holds references to all managed documents.
73
     *
74
     * Documents are grouped by their class name, and then indexed by the
75
     * serialized string of their database identifier field or, if the class
76
     * has no identifier, the SPL object hash. Serializing the identifier allows
77
     * differentiation of values that may be equal (via type juggling) but not
78
     * identical.
79
     *
80
     * Since all classes in a hierarchy must share the same identifier set,
81
     * we always take the root class name of the hierarchy.
82
     *
83
     * @var array
84
     */
85
    private $identityMap = [];
86
87
    /**
88
     * Map of all identifiers of managed documents.
89
     * Keys are object ids (spl_object_hash).
90
     *
91
     * @var array
92
     */
93
    private $documentIdentifiers = [];
94
95
    /**
96
     * Map of the original document data of managed documents.
97
     * Keys are object ids (spl_object_hash). This is used for calculating changesets
98
     * at commit time.
99
     *
100
     * @internal Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
101
     *           A value will only really be copied if the value in the document is modified
102
     *           by the user.
103
     *
104
     * @var array
105
     */
106
    private $originalDocumentData = [];
107
108
    /**
109
     * Map of document changes. Keys are object ids (spl_object_hash).
110
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
111
     *
112
     * @var array
113
     */
114
    private $documentChangeSets = [];
115
116
    /**
117
     * The (cached) states of any known documents.
118
     * Keys are object ids (spl_object_hash).
119
     *
120
     * @var array
121
     */
122
    private $documentStates = [];
123
124
    /**
125
     * Map of documents that are scheduled for dirty checking at commit time.
126
     *
127
     * Documents are grouped by their class name, and then indexed by their SPL
128
     * object hash. This is only used for documents with a change tracking
129
     * policy of DEFERRED_EXPLICIT.
130
     *
131
     * @var array
132
     * @todo rename: scheduledForSynchronization
133
     */
134
    private $scheduledForDirtyCheck = [];
135
136
    /**
137
     * A list of all pending document insertions.
138
     *
139
     * @var array
140
     */
141
    private $documentInsertions = [];
142
143
    /**
144
     * A list of all pending document updates.
145
     *
146
     * @var array
147
     */
148
    private $documentUpdates = [];
149
150
    /**
151
     * A list of all pending document upserts.
152
     *
153
     * @var array
154
     */
155
    private $documentUpserts = [];
156
157
    /**
158
     * A list of all pending document deletions.
159
     *
160
     * @var array
161
     */
162
    private $documentDeletions = [];
163
164
    /**
165
     * All pending collection deletions.
166
     *
167
     * @var array
168
     */
169
    private $collectionDeletions = [];
170
171
    /**
172
     * All pending collection updates.
173
     *
174
     * @var array
175
     */
176
    private $collectionUpdates = [];
177
178
    /**
179
     * A list of documents related to collections scheduled for update or deletion
180
     *
181
     * @var array
182
     */
183
    private $hasScheduledCollections = [];
184
185
    /**
186
     * List of collections visited during changeset calculation on a commit-phase of a UnitOfWork.
187
     * At the end of the UnitOfWork all these collections will make new snapshots
188
     * of their data.
189
     *
190
     * @var array
191
     */
192
    private $visitedCollections = [];
193
194
    /**
195
     * The DocumentManager that "owns" this UnitOfWork instance.
196
     *
197
     * @var DocumentManager
198
     */
199
    private $dm;
200
201
    /**
202
     * The EventManager used for dispatching events.
203
     *
204
     * @var EventManager
205
     */
206
    private $evm;
207
208
    /**
209
     * Additional documents that are scheduled for removal.
210
     *
211
     * @var array
212
     */
213
    private $orphanRemovals = [];
214
215
    /**
216
     * The HydratorFactory used for hydrating array Mongo documents to Doctrine object documents.
217
     *
218
     * @var HydratorFactory
219
     */
220
    private $hydratorFactory;
221
222
    /**
223
     * The document persister instances used to persist document instances.
224
     *
225
     * @var array
226
     */
227
    private $persisters = [];
228
229
    /**
230
     * The collection persister instance used to persist changes to collections.
231
     *
232
     * @var Persisters\CollectionPersister
233
     */
234
    private $collectionPersister;
235
236
    /**
237
     * The persistence builder instance used in DocumentPersisters.
238
     *
239
     * @var PersistenceBuilder|null
240
     */
241
    private $persistenceBuilder;
242
243
    /**
244
     * Array of parent associations between embedded documents.
245
     *
246
     * @var array
247
     */
248
    private $parentAssociations = [];
249
250
    /** @var LifecycleEventManager */
251
    private $lifecycleEventManager;
252
253
    /**
254
     * Array of embedded documents known to UnitOfWork. We need to hold them to prevent spl_object_hash
255
     * collisions in case already managed object is lost due to GC (so now it won't). Embedded documents
256
     * found during doDetach are removed from the registry, to empty it altogether clear() can be utilized.
257
     *
258
     * @var array
259
     */
260
    private $embeddedDocumentsRegistry = [];
261
262
    /** @var int */
263
    private $commitsInProgress = 0;
264
265
    /**
266
     * Initializes a new UnitOfWork instance, bound to the given DocumentManager.
267
     */
268 1649
    public function __construct(DocumentManager $dm, EventManager $evm, HydratorFactory $hydratorFactory)
269
    {
270 1649
        $this->dm                    = $dm;
271 1649
        $this->evm                   = $evm;
272 1649
        $this->hydratorFactory       = $hydratorFactory;
273 1649
        $this->lifecycleEventManager = new LifecycleEventManager($dm, $this, $evm);
274 1649
    }
275
276
    /**
277
     * Factory for returning new PersistenceBuilder instances used for preparing data into
278
     * queries for insert persistence.
279
     */
280 1130
    public function getPersistenceBuilder() : PersistenceBuilder
281
    {
282 1130
        if (! $this->persistenceBuilder) {
283 1130
            $this->persistenceBuilder = new PersistenceBuilder($this->dm, $this);
284
        }
285 1130
        return $this->persistenceBuilder;
286
    }
287
288
    /**
289
     * Sets the parent association for a given embedded document.
290
     */
291 209
    public function setParentAssociation(object $document, array $mapping, ?object $parent, string $propertyPath) : void
292
    {
293 209
        $oid                                   = spl_object_hash($document);
294 209
        $this->embeddedDocumentsRegistry[$oid] = $document;
295 209
        $this->parentAssociations[$oid]        = [$mapping, $parent, $propertyPath];
296 209
    }
297
298
    /**
299
     * Gets the parent association for a given embedded document.
300
     *
301
     *     <code>
302
     *     list($mapping, $parent, $propertyPath) = $this->getParentAssociation($embeddedDocument);
303
     *     </code>
304
     */
305 233
    public function getParentAssociation(object $document) : ?array
306
    {
307 233
        $oid = spl_object_hash($document);
308
309 233
        return $this->parentAssociations[$oid] ?? null;
310
    }
311
312
    /**
313
     * Get the document persister instance for the given document name
314
     */
315 1128
    public function getDocumentPersister(string $documentName) : Persisters\DocumentPersister
316
    {
317 1128
        if (! isset($this->persisters[$documentName])) {
318 1115
            $class                           = $this->dm->getClassMetadata($documentName);
319 1115
            $pb                              = $this->getPersistenceBuilder();
320 1115
            $this->persisters[$documentName] = new Persisters\DocumentPersister($pb, $this->dm, $this, $this->hydratorFactory, $class);
321
        }
322 1128
        return $this->persisters[$documentName];
323
    }
324
325
    /**
326
     * Get the collection persister instance.
327
     */
328 1128
    public function getCollectionPersister() : CollectionPersister
329
    {
330 1128
        if (! isset($this->collectionPersister)) {
331 1128
            $pb                        = $this->getPersistenceBuilder();
332 1128
            $this->collectionPersister = new Persisters\CollectionPersister($this->dm, $pb, $this);
333
        }
334 1128
        return $this->collectionPersister;
335
    }
336
337
    /**
338
     * Set the document persister instance to use for the given document name
339
     */
340 13
    public function setDocumentPersister(string $documentName, Persisters\DocumentPersister $persister) : void
341
    {
342 13
        $this->persisters[$documentName] = $persister;
343 13
    }
344
345
    /**
346
     * Commits the UnitOfWork, executing all operations that have been postponed
347
     * up to this point. The state of all managed documents will be synchronized with
348
     * the database.
349
     *
350
     * The operations are executed in the following order:
351
     *
352
     * 1) All document insertions
353
     * 2) All document updates
354
     * 3) All document deletions
355
     *
356
     * @param array $options Array of options to be used with batchInsert(), update() and remove()
357
     */
358 611
    public function commit(array $options = []) : void
359
    {
360
        // Raise preFlush
361 611
        if ($this->evm->hasListeners(Events::preFlush)) {
362
            $this->evm->dispatchEvent(Events::preFlush, new Event\PreFlushEventArgs($this->dm));
363
        }
364
365
        // Compute changes done since last commit.
366 611
        $this->computeChangeSets();
367
368 610
        if (! ($this->documentInsertions ||
0 ignored issues
show
Bug Best Practice introduced by Jonathan H. Wage
The expression $this->documentInsertions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
369 257
            $this->documentUpserts ||
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->documentUpserts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
370 215
            $this->documentDeletions ||
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->documentDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
371 198
            $this->documentUpdates ||
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->documentUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
372 22
            $this->collectionUpdates ||
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->collectionUpdates of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
373 22
            $this->collectionDeletions ||
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->collectionDeletions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
374 610
            $this->orphanRemovals)
0 ignored issues
show
Bug Best Practice introduced by Mike Graf
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
375
        ) {
376 22
            return; // Nothing to do.
377
        }
378
379 607
        $this->commitsInProgress++;
380 607
        if ($this->commitsInProgress > 1) {
381
            throw MongoDBException::commitInProgress();
382
        }
383
        try {
384 607
            if ($this->orphanRemovals) {
0 ignored issues
show
Bug Best Practice introduced by Bulat Shakirzyanov
The expression $this->orphanRemovals of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
385 55
                foreach ($this->orphanRemovals as $removal) {
386 55
                    $this->remove($removal);
387
                }
388
            }
389
390
            // Raise onFlush
391 607
            if ($this->evm->hasListeners(Events::onFlush)) {
392 5
                $this->evm->dispatchEvent(Events::onFlush, new Event\OnFlushEventArgs($this->dm));
393
            }
394
395 606
            foreach ($this->getClassesForCommitAction($this->documentUpserts) as $classAndDocuments) {
396 86
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $class does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
397 86
                $this->executeUpserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
398
            }
399
400 606
            foreach ($this->getClassesForCommitAction($this->documentInsertions) as $classAndDocuments) {
401 530
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
402 530
                $this->executeInserts($class, $documents, $options);
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
403
            }
404
405 593
            foreach ($this->getClassesForCommitAction($this->documentUpdates) as $classAndDocuments) {
406 236
                [$class, $documents] = $classAndDocuments;
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

Loading history...
407 236
                $this->executeUpdates($class, $documents, $options);
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $documents does not exist. Did you mean $classAndDocuments?

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

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

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

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

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

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

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

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

Loading history...
413
            }
414
415
            // Raise postFlush
416 593
            if ($this->evm->hasListeners(Events::postFlush)) {
417
                $this->evm->dispatchEvent(Events::postFlush, new Event\PostFlushEventArgs($this->dm));
418
            }
419
420
            // Clear up
421 593
            $this->documentInsertions      =
422 593
            $this->documentUpserts         =
423 593
            $this->documentUpdates         =
424 593
            $this->documentDeletions       =
425 593
            $this->documentChangeSets      =
426 593
            $this->collectionUpdates       =
427 593
            $this->collectionDeletions     =
428 593
            $this->visitedCollections      =
429 593
            $this->scheduledForDirtyCheck  =
430 593
            $this->orphanRemovals          =
431 593
            $this->hasScheduledCollections = [];
432 593
        } finally {
433 607
            $this->commitsInProgress--;
434
        }
435 593
    }
436
437
    /**
438
     * Groups a list of scheduled documents by their class.
439
     */
440 606
    private function getClassesForCommitAction(array $documents, bool $includeEmbedded = false) : array
441
    {
442 606
        if (empty($documents)) {
443 606
            return [];
444
        }
445 605
        $divided = [];
446 605
        $embeds  = [];
447 605
        foreach ($documents as $oid => $d) {
448 605
            $className = get_class($d);
449 605
            if (isset($embeds[$className])) {
450 81
                continue;
451
            }
452 605
            if (isset($divided[$className])) {
453 165
                $divided[$className][1][$oid] = $d;
454 165
                continue;
455
            }
456 605
            $class = $this->dm->getClassMetadata($className);
457 605
            if ($class->isEmbeddedDocument && ! $includeEmbedded) {
458 183
                $embeds[$className] = true;
459 183
                continue;
460
            }
461 605
            if (empty($divided[$class->name])) {
462 605
                $divided[$class->name] = [$class, [$oid => $d]];
463
            } else {
464 605
                $divided[$class->name][1][$oid] = $d;
465
            }
466
        }
467 605
        return $divided;
468
    }
469
470
    /**
471
     * Compute changesets of all documents scheduled for insertion.
472
     *
473
     * Embedded documents will not be processed.
474
     */
475 616
    private function computeScheduleInsertsChangeSets() : void
476
    {
477 616
        foreach ($this->documentInsertions as $document) {
478 543
            $class = $this->dm->getClassMetadata(get_class($document));
479 543
            if ($class->isEmbeddedDocument) {
480 165
                continue;
481
            }
482
483 537
            $this->computeChangeSet($class, $document);
484
        }
485 615
    }
486
487
    /**
488
     * Compute changesets of all documents scheduled for upsert.
489
     *
490
     * Embedded documents will not be processed.
491
     */
492 615
    private function computeScheduleUpsertsChangeSets() : void
493
    {
494 615
        foreach ($this->documentUpserts as $document) {
495 85
            $class = $this->dm->getClassMetadata(get_class($document));
496 85
            if ($class->isEmbeddedDocument) {
497
                continue;
498
            }
499
500 85
            $this->computeChangeSet($class, $document);
501
        }
502 615
    }
503
504
    /**
505
     * Gets the changeset for a document.
506
     *
507
     * @return array array('property' => array(0 => mixed|null, 1 => mixed|null))
508
     */
509 611
    public function getDocumentChangeSet(object $document) : array
510
    {
511 611
        $oid = spl_object_hash($document);
512
513 611
        return $this->documentChangeSets[$oid] ?? [];
514
    }
515
516
    /**
517
     * INTERNAL:
518
     * Sets the changeset for a document.
519
     */
520 1
    public function setDocumentChangeSet(object $document, array $changeset) : void
521
    {
522 1
        $this->documentChangeSets[spl_object_hash($document)] = $changeset;
523 1
    }
524
525
    /**
526
     * Get a documents actual data, flattening all the objects to arrays.
527
     *
528
     * @return array
529
     */
530 616
    public function getDocumentActualData(object $document) : array
531
    {
532 616
        $class      = $this->dm->getClassMetadata(get_class($document));
533 616
        $actualData = [];
534 616
        foreach ($class->reflFields as $name => $refProp) {
535 616
            $mapping = $class->fieldMappings[$name];
536
            // skip not saved fields
537 616
            if (isset($mapping['notSaved']) && $mapping['notSaved'] === true) {
538 197
                continue;
539
            }
540 616
            $value = $refProp->getValue($document);
541 616
            if ((isset($mapping['association']) && $mapping['type'] === 'many')
542 616
                && $value !== null && ! ($value instanceof PersistentCollectionInterface)) {
543
                // If $actualData[$name] is not a Collection then use an ArrayCollection.
544 400
                if (! $value instanceof Collection) {
0 ignored issues
show
Bug introduced by Bulat Shakirzyanov
The class Doctrine\Common\Collections\Collection does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
545 147
                    $value = new ArrayCollection($value);
546
                }
547
548
                // Inject PersistentCollection
549 400
                $coll = $this->dm->getConfiguration()->getPersistentCollectionFactory()->create($this->dm, $mapping, $value);
550 400
                $coll->setOwner($document, $mapping);
551 400
                $coll->setDirty(! $value->isEmpty());
552 400
                $class->reflFields[$name]->setValue($document, $coll);
553 400
                $actualData[$name] = $coll;
554
            } else {
555 616
                $actualData[$name] = $value;
556
            }
557
        }
558 616
        return $actualData;
559
    }
560
561
    /**
562
     * Computes the changes that happened to a single document.
563
     *
564
     * Modifies/populates the following properties:
565
     *
566
     * {@link originalDocumentData}
567
     * If the document is NEW or MANAGED but not yet fully persisted (only has an id)
568
     * then it was not fetched from the database and therefore we have no original
569
     * document data yet. All of the current document data is stored as the original document data.
570
     *
571
     * {@link documentChangeSets}
572
     * The changes detected on all properties of the document are stored there.
573
     * A change is a tuple array where the first entry is the old value and the second
574
     * entry is the new value of the property. Changesets are used by persisters
575
     * to INSERT/UPDATE the persistent document state.
576
     *
577
     * {@link documentUpdates}
578
     * If the document is already fully MANAGED (has been fetched from the database before)
579
     * and any changes to its properties are detected, then a reference to the document is stored
580
     * there to mark it for an update.
581
     */
582 612
    public function computeChangeSet(ClassMetadata $class, object $document) : void
583
    {
584 612
        if (! $class->isInheritanceTypeNone()) {
585 183
            $class = $this->dm->getClassMetadata(get_class($document));
586
        }
587
588
        // Fire PreFlush lifecycle callbacks
589 612
        if (! empty($class->lifecycleCallbacks[Events::preFlush])) {
590 11
            $class->invokeLifecycleCallbacks(Events::preFlush, $document, [new Event\PreFlushEventArgs($this->dm)]);
591
        }
592
593 612
        $this->computeOrRecomputeChangeSet($class, $document);
594 611
    }
595
596
    /**
597
     * Used to do the common work of computeChangeSet and recomputeSingleDocumentChangeSet
598
     */
599 612
    private function computeOrRecomputeChangeSet(ClassMetadata $class, object $document, bool $recompute = false) : void
600
    {
601 612
        $oid           = spl_object_hash($document);
602 612
        $actualData    = $this->getDocumentActualData($document);
603 612
        $isNewDocument = ! isset($this->originalDocumentData[$oid]);
604 612
        if ($isNewDocument) {
605
            // Document is either NEW or MANAGED but not yet fully persisted (only has an id).
606
            // These result in an INSERT.
607 611
            $this->originalDocumentData[$oid] = $actualData;
608 611
            $changeSet                        = [];
609 611
            foreach ($actualData as $propName => $actualValue) {
610
                /* At this PersistentCollection shouldn't be here, probably it
611
                 * was cloned and its ownership must be fixed
612
                 */
613 611
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
614
                    $actualData[$propName] = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
615
                    $actualValue           = $actualData[$propName];
616
                }
617
                // ignore inverse side of reference relationship
618 611
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
619 187
                    continue;
620
                }
621 611
                $changeSet[$propName] = [null, $actualValue];
622
            }
623 611
            $this->documentChangeSets[$oid] = $changeSet;
624
        } else {
625 297
            if ($class->isReadOnly) {
626 2
                return;
627
            }
628
            // Document is "fully" MANAGED: it was already fully persisted before
629
            // and we have a copy of the original data
630 295
            $originalData           = $this->originalDocumentData[$oid];
631 295
            $isChangeTrackingNotify = $class->isChangeTrackingNotify();
632 295
            if ($isChangeTrackingNotify && ! $recompute && isset($this->documentChangeSets[$oid])) {
633 2
                $changeSet = $this->documentChangeSets[$oid];
634
            } else {
635 295
                $changeSet = [];
636
            }
637
638 295
            $gridFSMetadataProperty = null;
639
640 295
            if ($class->isFile) {
641
                try {
642 4
                    $gridFSMetadata         = $class->getFieldMappingByDbFieldName('metadata');
643 3
                    $gridFSMetadataProperty = $gridFSMetadata['fieldName'];
644 1
                } catch (MappingException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by Andreas Braun
Consider adding a comment why this CATCH block is empty.
Loading history...
645
                }
646
            }
647
648 295
            foreach ($actualData as $propName => $actualValue) {
649
                // skip not saved fields
650 295
                if ((isset($class->fieldMappings[$propName]['notSaved']) && $class->fieldMappings[$propName]['notSaved'] === true) ||
651 295
                    ($class->isFile && $propName !== $gridFSMetadataProperty)) {
652 4
                    continue;
653
                }
654
655 294
                $orgValue = $originalData[$propName] ?? null;
656
657
                // skip if value has not changed
658 294
                if ($orgValue === $actualValue) {
659 292
                    if (! $actualValue instanceof PersistentCollectionInterface) {
660 292
                        continue;
661
                    }
662
663 206
                    if (! $actualValue->isDirty() && ! $this->isCollectionScheduledForDeletion($actualValue)) {
664
                        // consider dirty collections as changed as well
665 182
                        continue;
666
                    }
667
                }
668
669
                // if relationship is a embed-one, schedule orphan removal to trigger cascade remove operations
670 254
                if (isset($class->fieldMappings[$propName]['embedded']) && $class->fieldMappings[$propName]['type'] === 'one') {
671 14
                    if ($orgValue !== null) {
672 8
                        $this->scheduleOrphanRemoval($orgValue);
673
                    }
674 14
                    $changeSet[$propName] = [$orgValue, $actualValue];
675 14
                    continue;
676
                }
677
678
                // if owning side of reference-one relationship
679 247
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide']) {
680 13
                    if ($orgValue !== null && $class->fieldMappings[$propName]['orphanRemoval']) {
681 1
                        $this->scheduleOrphanRemoval($orgValue);
682
                    }
683
684 13
                    $changeSet[$propName] = [$orgValue, $actualValue];
685 13
                    continue;
686
                }
687
688 240
                if ($isChangeTrackingNotify) {
689 3
                    continue;
690
                }
691
692
                // ignore inverse side of reference relationship
693 238
                if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
694 6
                    continue;
695
                }
696
697
                // Persistent collection was exchanged with the "originally"
698
                // created one. This can only mean it was cloned and replaced
699
                // on another document.
700 236
                if ($actualValue instanceof PersistentCollectionInterface && $actualValue->getOwner() !== $document) {
701 6
                    $actualValue = $this->fixPersistentCollectionOwnership($actualValue, $document, $class, $propName);
702
                }
703
704
                // if embed-many or reference-many relationship
705 236
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many') {
706 125
                    $changeSet[$propName] = [$orgValue, $actualValue];
707
                    /* If original collection was exchanged with a non-empty value
708
                     * and $set will be issued, there is no need to $unset it first
709
                     */
710 125
                    if ($actualValue && $actualValue->isDirty() && CollectionHelper::usesSet($class->fieldMappings[$propName]['strategy'])) {
711 31
                        continue;
712
                    }
713 104
                    if ($orgValue !== $actualValue && $orgValue instanceof PersistentCollectionInterface) {
714 18
                        $this->scheduleCollectionDeletion($orgValue);
715
                    }
716 104
                    continue;
717
                }
718
719
                // skip equivalent date values
720 148
                if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'date') {
721
                    /** @var DateType $dateType */
722 37
                    $dateType      = Type::getType('date');
723 37
                    $dbOrgValue    = $dateType->convertToDatabaseValue($orgValue);
724 37
                    $dbActualValue = $dateType->convertToDatabaseValue($actualValue);
725
726 37
                    $orgTimestamp    = $dbOrgValue instanceof UTCDateTime ? $dbOrgValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by Andreas Braun
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
727 37
                    $actualTimestamp = $dbActualValue instanceof UTCDateTime ? $dbActualValue->toDateTime()->getTimestamp() : null;
0 ignored issues
show
Bug introduced by Andreas Braun
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
728
729 37
                    if ($orgTimestamp === $actualTimestamp) {
730 30
                        continue;
731
                    }
732
                }
733
734
                // regular field
735 131
                $changeSet[$propName] = [$orgValue, $actualValue];
736
            }
737 295
            if ($changeSet) {
738 243
                $this->documentChangeSets[$oid] = isset($this->documentChangeSets[$oid])
739 19
                    ? $changeSet + $this->documentChangeSets[$oid]
740 240
                    : $changeSet;
741
742 243
                $this->originalDocumentData[$oid] = $actualData;
743 243
                $this->scheduleForUpdate($document);
744
            }
745
        }
746
747
        // Look for changes in associations of the document
748 612
        $associationMappings = array_filter(
749 612
            $class->associationMappings,
750
            static function ($assoc) {
751 471
                return empty($assoc['notSaved']);
752 612
            }
753
        );
754
755 612
        foreach ($associationMappings as $mapping) {
756 471
            $value = $class->reflFields[$mapping['fieldName']]->getValue($document);
757
758 471
            if ($value === null) {
759 323
                continue;
760
            }
761
762 452
            $this->computeAssociationChanges($document, $mapping, $value);
763
764 451
            if (isset($mapping['reference'])) {
765 337
                continue;
766
            }
767
768 354
            $values = $mapping['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
769
770 354
            foreach ($values as $obj) {
771 188
                $oid2 = spl_object_hash($obj);
772
773 188
                if (isset($this->documentChangeSets[$oid2])) {
774 186
                    if (empty($this->documentChangeSets[$oid][$mapping['fieldName']])) {
775
                        // instance of $value is the same as it was previously otherwise there would be
776
                        // change set already in place
777 42
                        $this->documentChangeSets[$oid][$mapping['fieldName']] = [$value, $value];
778
                    }
779
780 186
                    if (! $isNewDocument) {
781 87
                        $this->scheduleForUpdate($document);
782
                    }
783
784 354
                    break;
785
                }
786
            }
787
        }
788 611
    }
789
790
    /**
791
     * Computes all the changes that have been done to documents and collections
792
     * since the last commit and stores these changes in the _documentChangeSet map
793
     * temporarily for access by the persisters, until the UoW commit is finished.
794
     */
795 616
    public function computeChangeSets() : void
796
    {
797 616
        $this->computeScheduleInsertsChangeSets();
798 615
        $this->computeScheduleUpsertsChangeSets();
799
800
        // Compute changes for other MANAGED documents. Change tracking policies take effect here.
801 615
        foreach ($this->identityMap as $className => $documents) {
802 615
            $class = $this->dm->getClassMetadata($className);
803 615
            if ($class->isEmbeddedDocument) {
804
                /* we do not want to compute changes to embedded documents up front
805
                 * in case embedded document was replaced and its changeset
806
                 * would corrupt data. Embedded documents' change set will
807
                 * be calculated by reachability from owning document.
808
                 */
809 178
                continue;
810
            }
811
812
            // If change tracking is explicit or happens through notification, then only compute
813
            // changes on document of that type that are explicitly marked for synchronization.
814
            switch (true) {
815 615
                case $class->isChangeTrackingDeferredImplicit():
816 614
                    $documentsToProcess = $documents;
817 614
                    break;
818
819 4
                case isset($this->scheduledForDirtyCheck[$className]):
820 3
                    $documentsToProcess = $this->scheduledForDirtyCheck[$className];
821 3
                    break;
822
823
                default:
824 4
                    $documentsToProcess = [];
825
            }
826
827 615
            foreach ($documentsToProcess as $document) {
828
                // Ignore uninitialized proxy objects
829 610
                if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by Andreas Braun
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
830 9
                    continue;
831
                }
832
                // Only MANAGED documents that are NOT SCHEDULED FOR INSERTION, UPSERT OR DELETION are processed here.
833 610
                $oid = spl_object_hash($document);
834 610
                if (isset($this->documentInsertions[$oid])
835 338
                    || isset($this->documentUpserts[$oid])
836 292
                    || isset($this->documentDeletions[$oid])
837 610
                    || ! isset($this->documentStates[$oid])
838
                ) {
839 608
                    continue;
840
                }
841
842 615
                $this->computeChangeSet($class, $document);
843
            }
844
        }
845 615
    }
846
847
    /**
848
     * Computes the changes of an association.
849
     *
850
     * @param mixed $value The value of the association.
851
     *
852
     * @throws InvalidArgumentException
853
     */
854 452
    private function computeAssociationChanges(object $parentDocument, array $assoc, $value) : void
855
    {
856 452
        $isNewParentDocument   = isset($this->documentInsertions[spl_object_hash($parentDocument)]);
857 452
        $class                 = $this->dm->getClassMetadata(get_class($parentDocument));
858 452
        $topOrExistingDocument = ( ! $isNewParentDocument || ! $class->isEmbeddedDocument);
859
860 452
        if ($value instanceof GhostObjectInterface && ! $value->isProxyInitialized()) {
0 ignored issues
show
Bug introduced by Andreas Braun
The class ProxyManager\Proxy\GhostObjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

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

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

2. Missing use statement

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

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

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

Loading history...
861 7
            return;
862
        }
863
864 451
        if ($value instanceof PersistentCollectionInterface && $value->isDirty() && $value->getOwner() !== null && ($assoc['isOwningSide'] || isset($assoc['embedded']))) {
865 258
            if ($topOrExistingDocument || CollectionHelper::usesSet($assoc['strategy'])) {
866 254
                $this->scheduleCollectionUpdate($value);
867
            }
868
869 258
            $topmostOwner                                               = $this->getOwningDocument($value->getOwner());
870 258
            $this->visitedCollections[spl_object_hash($topmostOwner)][] = $value;
871 258
            if (! empty($assoc['orphanRemoval']) || isset($assoc['embedded'])) {
872 151
                $value->initialize();
873 151
                foreach ($value->getDeletedDocuments() as $orphan) {
874 25
                    $this->scheduleOrphanRemoval($orphan);
875
                }
876
            }
877
        }
878
879
        // Look through the documents, and in any of their associations,
880
        // for transient (new) documents, recursively. ("Persistence by reachability")
881
        // Unwrap. Uninitialized collections will simply be empty.
882 451
        $unwrappedValue = $assoc['type'] === ClassMetadata::ONE ? [$value] : $value->unwrap();
883
884 451
        $count = 0;
885 451
        foreach ($unwrappedValue as $key => $entry) {
886 367
            if (! is_object($entry)) {
887 1
                throw new InvalidArgumentException(
888 1
                    sprintf('Expected object, found "%s" in %s::%s', $entry, get_class($parentDocument), $assoc['name'])
889
                );
890
            }
891
892 366
            $targetClass = $this->dm->getClassMetadata(get_class($entry));
893
894 366
            $state = $this->getDocumentState($entry, self::STATE_NEW);
895
896
            // Handle "set" strategy for multi-level hierarchy
897 366
            $pathKey = ! isset($assoc['strategy']) || CollectionHelper::isList($assoc['strategy']) ? $count : $key;
898 366
            $path    = $assoc['type'] === 'many' ? $assoc['name'] . '.' . $pathKey : $assoc['name'];
899
900 366
            $count++;
901
902
            switch ($state) {
903 366
                case self::STATE_NEW:
904 70
                    if (! $assoc['isCascadePersist']) {
905
                        throw new InvalidArgumentException('A new document was found through a relationship that was not'
906
                            . ' configured to cascade persist operations: ' . $this->objToStr($entry) . '.'
907
                            . ' Explicitly persist the new document or configure cascading persist operations'
908
                            . ' on the relationship.');
909
                    }
910
911 70
                    $this->persistNew($targetClass, $entry);
912 70
                    $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
913 70
                    $this->computeChangeSet($targetClass, $entry);
914 70
                    break;
915
916 362
                case self::STATE_MANAGED:
917 362
                    if ($targetClass->isEmbeddedDocument) {
918 179
                        [, $knownParent ] = $this->getParentAssociation($entry);
0 ignored issues
show
Bug introduced by Andreas Braun
The variable $knownParent does not exist. Did you forget to declare it?

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

Loading history...
919 179
                        if ($knownParent && $knownParent !== $parentDocument) {
920 6
                            $entry = clone $entry;
921 6
                            if ($assoc['type'] === ClassMetadata::ONE) {
922 3
                                $class->setFieldValue($parentDocument, $assoc['fieldName'], $entry);
923 3
                                $this->setOriginalDocumentProperty(spl_object_hash($parentDocument), $assoc['fieldName'], $entry);
924 3
                                $poid = spl_object_hash($parentDocument);
925 3
                                if (isset($this->documentChangeSets[$poid][$assoc['fieldName']])) {
926 3
                                    $this->documentChangeSets[$poid][$assoc['fieldName']][1] = $entry;
927
                                }
928
                            } else {
929
                                // must use unwrapped value to not trigger orphan removal
930 4
                                $unwrappedValue[$key] = $entry;
931
                            }
932 6
                            $this->persistNew($targetClass, $entry);
933
                        }
934 179
                        $this->setParentAssociation($entry, $assoc, $parentDocument, $path);
935 179
                        $this->computeChangeSet($targetClass, $entry);
936
                    }
937 362
                    break;
938
939 1
                case self::STATE_REMOVED:
940
                    // Consume the $value as array (it's either an array or an ArrayAccess)
941
                    // and remove the element from Collection.
942 1
                    if ($assoc['type'] === ClassMetadata::MANY) {
943
                        unset($value[$key]);
944
                    }
945 1
                    break;
946
947
                case self::STATE_DETACHED:
948
                    // Can actually not happen right now as we assume STATE_NEW,
949
                    // so the exception will be raised from the DBAL layer (constraint violation).
950
                    throw new InvalidArgumentException('A detached document was found through a '
951
                        . 'relationship during cascading a persist operation.');
952
953 366
                default:
954
                    // MANAGED associated documents are already taken into account
955
                    // during changeset calculation anyway, since they are in the identity map.
956
            }
957
        }
958 450
    }
959
960
    /**
961
     * INTERNAL:
962
     * Computes the changeset of an individual document, independently of the
963
     * computeChangeSets() routine that is used at the beginning of a UnitOfWork#commit().
964
     *
965
     * The passed document must be a managed document. If the document already has a change set
966
     * because this method is invoked during a commit cycle then the change sets are added.
967
     * whereby changes detected in this method prevail.
968
     *
969
     * @throws InvalidArgumentException If the passed document is not MANAGED.
970
     *
971
     * @ignore
972
     */
973 19
    public function recomputeSingleDocumentChangeSet(ClassMetadata $class, object $document) : void
974
    {
975
        // Ignore uninitialized proxy objects
976 19
        if ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {