Completed
Pull Request — master (#1880)
by
unknown
16:03
created

CollectionPersister   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 574
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 86.03%

Importance

Changes 0
Metric Value
wmc 78
lcom 1
cbo 6
dl 0
loc 574
ccs 234
cts 272
cp 0.8603
rs 2.16
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A deleteAll() 0 24 5
A delete() 0 13 3
B update() 0 29 8
B updateAll() 0 41 11
A setCollection() 0 9 1
A setCollections() 0 30 4
A deleteElements() 0 26 3
B deleteCollections() 0 50 8
B insertElements() 0 39 5
B insertCollections() 0 62 8
A pushAllCollections() 0 24 4
A addToSetCollections() 0 19 3
A getValuePrepareCallback() 0 13 2
A getPathAndParent() 0 20 4
A executeQuery() 0 15 4
A excludeSubPaths() 0 17 4

How to fix   Complexity   

Complex Class

Complex classes like CollectionPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CollectionPersister, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\Persisters;
6
7
use Closure;
8
use Doctrine\ODM\MongoDB\DocumentManager;
9
use Doctrine\ODM\MongoDB\LockException;
10
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
11
use Doctrine\ODM\MongoDB\PersistentCollection\PersistentCollectionInterface;
12
use Doctrine\ODM\MongoDB\UnitOfWork;
13
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
14
use LogicException;
15
use UnexpectedValueException;
16
use function array_diff_key;
17
use function array_fill_keys;
18
use function array_flip;
19
use function array_intersect_key;
20
use function array_keys;
21
use function array_map;
22
use function array_reverse;
23
use function array_values;
24
use function count;
25
use function end;
26
use function get_class;
27
use function implode;
28
use function sort;
29
use function sprintf;
30
use function strpos;
31
32
/**
33
 * The CollectionPersister is responsible for persisting collections of embedded
34
 * or referenced documents. When a PersistentCollection is scheduledForDeletion
35
 * in the UnitOfWork by calling PersistentCollection::clear() or is
36
 * de-referenced in the domain application code, CollectionPersister::delete()
37
 * will be called. When documents within the PersistentCollection are added or
38
 * removed, CollectionPersister::update() will be called, which may set the
39
 * entire collection or delete/insert individual elements, depending on the
40
 * mapping strategy.
41
 */
42
class CollectionPersister
43
{
44
    /** @var DocumentManager */
45
    private $dm;
46
47
    /** @var PersistenceBuilder */
48
    private $pb;
49
50
    /** @var UnitOfWork */
51
    private $uow;
52
53 1131
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
54
    {
55 1131
        $this->dm  = $dm;
56 1131
        $this->pb  = $pb;
57 1131
        $this->uow = $uow;
58 1131
    }
59
60
    /**
61
     * Deletes a PersistentCollection instances completely from a document using $unset.
62
     *
63
     * @param PersistentCollectionInterface[] $collections
64
     * @param array                           $options
65
     */
66 37
    public function deleteAll(object $parent, array $collections, array $options) : void
67
    {
68 37
        $unsetPathsMap = [];
69
70 37
        foreach ($collections as $collection) {
71 37
            $mapping = $collection->getMapping();
72 37
            if ($mapping['isInverseSide']) {
73
                continue; // ignore inverse side
74
            }
75 37
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
76
                throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
77
            }
78 37
            [$propertyPath ]              = $this->getPathAndParent($collection);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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...
79 37
            $unsetPathsMap[$propertyPath] = true;
80
        }
81
82 37
        if (empty($unsetPathsMap)) {
83
            return;
84
        }
85
86 37
        $unsetPaths = array_fill_keys($this->excludeSubPaths(array_keys($unsetPathsMap)), true);
87 37
        $query      = ['$unset' => $unsetPaths];
88 37
        $this->executeQuery($parent, $query, $options);
89 37
    }
90
91
    /**
92
     * Deletes a PersistentCollection instance completely from a document using $unset.
93
     */
94 4
    public function delete(PersistentCollectionInterface $coll, array $options) : void
95
    {
96 4
        $mapping = $coll->getMapping();
97 4
        if ($mapping['isInverseSide']) {
98
            return; // ignore inverse side
99
        }
100 4
        if (CollectionHelper::isAtomic($mapping['strategy'])) {
101
            throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
102
        }
103 4
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $parent 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...
104 4
        $query                   = ['$unset' => [$propertyPath => true]];
105 4
        $this->executeQuery($parent, $query, $options);
106 3
    }
107
108
    /**
109
     * Updates a PersistentCollection instance deleting removed rows and
110
     * inserting new rows.
111
     */
112 1
    public function update(PersistentCollectionInterface $coll, array $options) : void
113
    {
114 1
        $mapping = $coll->getMapping();
115
116 1
        if ($mapping['isInverseSide']) {
117
            return; // ignore inverse side
118
        }
119
120 1
        switch ($mapping['strategy']) {
121
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
122
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
123
                throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
124
125
            case ClassMetadata::STORAGE_STRATEGY_SET:
126
            case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
127
                $this->setCollection($coll, $options);
128
                break;
129
130
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
131
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
132 1
                $coll->initialize();
133 1
                $this->deleteElements($coll, $options);
134 1
                $this->insertElements($coll, $options);
135
                break;
136
137
            default:
138
                throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
139
        }
140
    }
141
142
    /**
143
     * Updates a list PersistentCollection instances deleting removed rows and inserting new rows.
144
     *
145
     * @param PersistentCollectionInterface[] $collections
146
     * @param array                           $options
147
     */
148 105
    public function updateAll(object $parent, array $collections, array $options) : void
149
    {
150 105
        $setStrategyColls     = [];
151 105
        $addPushStrategyColls = [];
152
153 105
        foreach ($collections as $coll) {
154 105
            $mapping = $coll->getMapping();
155
156 105
            if ($mapping['isInverseSide']) {
157
                continue; // ignore inverse side
158
            }
159 105
            switch ($mapping['strategy']) {
160
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
161
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
162
                    throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
163
164
                case ClassMetadata::STORAGE_STRATEGY_SET:
165
                case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
166 14
                    $setStrategyColls[] = $coll;
167 14
                    break;
168
169
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
170
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
171 96
                    $addPushStrategyColls[] = $coll;
172 96
                    break;
173
174
                default:
175 105
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
176
            }
177
        }
178
179 105
        if (! empty($setStrategyColls)) {
180 14
            $this->setCollections($parent, $setStrategyColls, $options);
181
        }
182 105
        if (empty($addPushStrategyColls)) {
183 9
            return;
184
        }
185
186 96
        $this->deleteCollections($parent, $addPushStrategyColls, $options);
187 96
        $this->insertCollections($parent, $addPushStrategyColls, $options);
188 96
    }
189
190
    /**
191
     * Sets a PersistentCollection instance.
192
     *
193
     * This method is intended to be used with the "set" or "setArray"
194
     * strategies. The "setArray" strategy will ensure that the collection is
195
     * set as a BSON array, which means the collection elements will be
196
     * reindexed numerically before storage.
197
     */
198
    private function setCollection(PersistentCollectionInterface $coll, array $options) : void
199
    {
200
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $parent 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...
201
        $coll->initialize();
202
        $mapping = $coll->getMapping();
203
        $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy']));
204
        $query   = ['$set' => [$propertyPath => $setData]];
205
        $this->executeQuery($parent, $query, $options);
206
    }
207
208
    /**
209
     * Sets a list of PersistentCollection instances.
210
     *
211
     * This method is intended to be used with the "set" or "setArray"
212
     * strategies. The "setArray" strategy will ensure that the collection is
213
     * set as a BSON array, which means the collection elements will be
214
     * reindexed numerically before storage.
215
     *
216
     * @param PersistentCollectionInterface[] $collections
217
     * @param array                           $options
218
     */
219 14
    private function setCollections(object $parent, array $collections, array $options) : void
220
    {
221 14
        $pathCollMap = [];
222 14
        $paths       = [];
223 14
        foreach ($collections as $coll) {
224 14
            [$propertyPath ]            = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
225 14
            $pathCollMap[$propertyPath] = $coll;
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
226 14
            $paths[]                    = $propertyPath;
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
227
        }
228
229 14
        $paths = $this->excludeSubPaths($paths);
230
        /** @var PersistentCollectionInterface[] $setColls */
231 14
        $setColls   = array_intersect_key($pathCollMap, array_flip($paths));
232 14
        $setPayload = [];
233 14
        foreach ($setColls as $propertyPath => $coll) {
234 14
            $coll->initialize();
235 14
            $mapping                   = $coll->getMapping();
236 14
            $setData                   = $this->pb->prepareAssociatedCollectionValue(
237 14
                $coll,
238 14
                CollectionHelper::usesSet($mapping['strategy'])
239
            );
240 14
            $setPayload[$propertyPath] = $setData;
241
        }
242 14
        if (empty($setPayload)) {
243
            return;
244
        }
245
246 14
        $query = ['$set' => $setPayload];
247 14
        $this->executeQuery($parent, $query, $options);
248 14
    }
249
250
    /**
251
     * Deletes removed elements from a PersistentCollection instance.
252
     *
253
     * This method is intended to be used with the "pushAll" and "addToSet"
254
     * strategies.
255
     */
256 1
    private function deleteElements(PersistentCollectionInterface $coll, array $options) : void
257
    {
258 1
        $deleteDiff = $coll->getDeleteDiff();
259
260 1
        if (empty($deleteDiff)) {
261 1
            return;
262
        }
263
264
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $parent 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...
265
266
        $query = ['$unset' => []];
267
268
        foreach ($deleteDiff as $key => $document) {
269
            $query['$unset'][$propertyPath . '.' . $key] = true;
270
        }
271
272
        $this->executeQuery($parent, $query, $options);
273
274
        /**
275
         * @todo This is a hack right now because we don't have a proper way to
276
         * remove an element from an array by its key. Unsetting the key results
277
         * in the element being left in the array as null so we have to pull
278
         * null values.
279
         */
280
        $this->executeQuery($parent, ['$pull' => [$propertyPath => null]], $options);
281
    }
282
283
    /**
284
     * Deletes removed elements from a list of PersistentCollection instances.
285
     *
286
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
287
     *
288
     * @param PersistentCollectionInterface[] $collections
289
     * @param array                           $options
290
     */
291 96
    private function deleteCollections(object $parent, array $collections, array $options) : void
292
    {
293 96
        $pathCollMap   = [];
294 96
        $paths         = [];
295 96
        $deleteDiffMap = [];
296
297 96
        foreach ($collections as $coll) {
298 96
            $coll->initialize();
299 96
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
300 2
                continue;
301
            }
302 94
            $deleteDiff = $coll->getDeleteDiff();
303
304 94
            if (empty($deleteDiff)) {
305 72
                continue;
306
            }
307 31
            [$propertyPath ] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
308
309 31
            $pathCollMap[$propertyPath]   = $coll;
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
310 31
            $paths[]                      = $propertyPath;
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
311 31
            $deleteDiffMap[$propertyPath] = $deleteDiff;
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
312
        }
313
314 96
        $paths        = $this->excludeSubPaths($paths);
315 96
        $deleteColls  = array_intersect_key($pathCollMap, array_flip($paths));
316 96
        $unsetPayload = [];
317 96
        $pullPayload  = [];
318 96
        foreach ($deleteColls as $propertyPath => $coll) {
319 31
            $deleteDiff = $deleteDiffMap[$propertyPath];
320 31
            foreach ($deleteDiff as $key => $document) {
321 31
                $unsetPayload[$propertyPath . '.' . $key] = true;
322
            }
323 31
            $pullPayload[$propertyPath] = null;
324
        }
325
326 96
        if (! empty($unsetPayload)) {
327 31
            $this->executeQuery($parent, ['$unset' => $unsetPayload], $options);
328
        }
329 96
        if (empty($pullPayload)) {
330 74
            return;
331
        }
332
333
        /**
334
         * @todo This is a hack right now because we don't have a proper way to
335
         * remove an element from an array by its key. Unsetting the key results
336
         * in the element being left in the array as null so we have to pull
337
         * null values.
338
         */
339 31
        $this->executeQuery($parent, ['$pull' => $pullPayload], $options);
340 31
    }
341
342
    /**
343
     * Inserts new elements for a PersistentCollection instance.
344
     *
345
     * This method is intended to be used with the "pushAll" and "addToSet"
346
     * strategies.
347
     */
348 1
    private function insertElements(PersistentCollectionInterface $coll, array $options) : void
349
    {
350 1
        $insertDiff = $coll->getInsertDiff();
351
352 1
        if (empty($insertDiff)) {
353
            return;
354
        }
355
356 1
        $mapping = $coll->getMapping();
357
358 1
        switch ($mapping['strategy']) {
359
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
360 1
                $operator = 'push';
361 1
                break;
362
363
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
364
                $operator = 'addToSet';
365
                break;
366
367
            default:
368
                throw new LogicException(sprintf('Invalid strategy %s given for insertElements', $mapping['strategy']));
369
        }
370
371 1
        [$propertyPath, $parent] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $parent 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...
372
373 1
        $callback = isset($mapping['embedded'])
374
            ? function ($v) use ($mapping) {
375 1
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
376 1
            }
377
            : function ($v) use ($mapping) {
378
                return $this->pb->prepareReferencedDocumentValue($mapping, $v);
379 1
            };
380
381 1
        $value = array_values(array_map($callback, $insertDiff));
382
383 1
        $query = ['$' . $operator => [$propertyPath => ['$each' => $value]]];
384
385 1
        $this->executeQuery($parent, $query, $options);
386
    }
387
388
    /**
389
     * Inserts new elements for a list of PersistentCollection instances.
390
     *
391
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
392
     *
393
     * @param PersistentCollectionInterface[] $collections
394
     * @param array                           $options
395
     */
396 96
    private function insertCollections(object $parent, array $collections, array $options) : void
397
    {
398 96
        $pushAllPathCollMap  = [];
399 96
        $addToSetPathCollMap = [];
400 96
        $pushAllPaths        = [];
401 96
        $addToSetPaths       = [];
402 96
        $diffsMap            = [];
403
404 96
        foreach ($collections as $coll) {
405 96
            $coll->initialize();
406 96
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
407 2
                continue;
408
            }
409 94
            $insertDiff = $coll->getInsertDiff();
410
411 94
            if (empty($insertDiff)) {
412 21
                continue;
413
            }
414
415 79
            $mapping  = $coll->getMapping();
416 79
            $strategy = $mapping['strategy'];
417
418 79
            [$propertyPath ]         = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath 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...
419 79
            $diffsMap[$propertyPath] = $insertDiff;
420
421 79
            switch ($strategy) {
422
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
423 74
                    $pushAllPathCollMap[$propertyPath] = $coll;
424 74
                    $pushAllPaths[]                    = $propertyPath;
425 74
                    break;
426
427
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
428 7
                    $addToSetPathCollMap[$propertyPath] = $coll;
429 7
                    $addToSetPaths[]                    = $propertyPath;
430 7
                    break;
431
432
                default:
433 79
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
434
            }
435
        }
436
437 96
        if (! empty($pushAllPaths)) {
438 74
            $this->pushAllCollections(
439 74
                $parent,
440 74
                $pushAllPaths,
441 74
                $pushAllPathCollMap,
442 74
                $diffsMap,
443 74
                $options
444
            );
445
        }
446 96
        if (empty($addToSetPaths)) {
447 90
            return;
448
        }
449
450 7
        $this->addToSetCollections(
451 7
            $parent,
452 7
            $addToSetPaths,
453 7
            $addToSetPathCollMap,
454 7
            $diffsMap,
455 7
            $options
456
        );
457 7
    }
458
459
    /**
460
     * Perform collections update for 'pushAll' strategy.
461
     *
462
     * @param object $parent       Parent object to which passed collections is belong.
463
     * @param array  $collsPaths   Paths of collections that is passed.
464
     * @param array  $pathCollsMap List of collections indexed by their paths.
465
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
466
     * @param array  $options
467
     */
468 74
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
469
    {
470 74
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
471
        /** @var PersistentCollectionInterface[] $pushAllColls */
472 74
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
473 74
        $pushAllPayload = [];
474 74
        foreach ($pushAllColls as $propertyPath => $coll) {
475 74
            $callback                      = $this->getValuePrepareCallback($coll);
476 74
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
477 74
            $pushAllPayload[$propertyPath] = ['$each' => $value];
478
        }
479
480 74
        if (! empty($pushAllPayload)) {
481 74
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
482
        }
483
484 74
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
485 74
        foreach ($pushAllColls as $propertyPath => $coll) {
486 2
            $callback = $this->getValuePrepareCallback($coll);
487 2
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
488 2
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
489 2
            $this->executeQuery($parent, $query, $options);
490
        }
491 74
    }
492
493
    /**
494
     * Perform collections update by 'addToSet' strategy.
495
     *
496
     * @param object $parent       Parent object to which passed collections is belong.
497
     * @param array  $collsPaths   Paths of collections that is passed.
498
     * @param array  $pathCollsMap List of collections indexed by their paths.
499
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
500
     * @param array  $options
501
     */
502 7
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
503
    {
504 7
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
505
        /** @var PersistentCollectionInterface[] $addToSetColls */
506 7
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
507
508 7
        $addToSetPayload = [];
509 7
        foreach ($addToSetColls as $propertyPath => $coll) {
510 7
            $callback                       = $this->getValuePrepareCallback($coll);
511 7
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
512 7
            $addToSetPayload[$propertyPath] = ['$each' => $value];
513
        }
514
515 7
        if (empty($addToSetPayload)) {
516
            return;
517
        }
518
519 7
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
520 7
    }
521
522
    /**
523
     * Return callback instance for specified collection. This callback will prepare values for query from documents
524
     * that collection contain.
525
     */
526 79
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
527
    {
528 79
        $mapping = $coll->getMapping();
529 79
        if (isset($mapping['embedded'])) {
530
            return function ($v) use ($mapping) {
531 43
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
532 43
            };
533
        }
534
535
        return function ($v) use ($mapping) {
536 37
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
537 37
        };
538
    }
539
540
    /**
541
     * Gets the parent information for a given PersistentCollection. It will
542
     * retrieve the top-level persistent Document that the PersistentCollection
543
     * lives in. We can use this to issue queries when updating a
544
     * PersistentCollection that is multiple levels deep inside an embedded
545
     * document.
546
     *
547
     *     <code>
548
     *     list($path, $parent) = $this->getPathAndParent($coll)
549
     *     </code>
550
     */
551 121
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
552
    {
553 121
        $mapping = $coll->getMapping();
554 121
        $fields  = [];
555 121
        $parent  = $coll->getOwner();
556 121
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
557 19
            [$m, $owner, $field] = $association;
0 ignored issues
show
Bug introduced by
The variable $m does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $owner does not exist. Did you forget to declare it?

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

Loading history...
Bug introduced by
The variable $field does not exist. Did you mean $fields?

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...
558 19
            if (isset($m['reference'])) {
559
                break;
560
            }
561 19
            $parent   = $owner;
562 19
            $fields[] = $field;
0 ignored issues
show
Bug introduced by
The variable $field does not exist. Did you mean $fields?

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...
563
        }
564 121
        $propertyPath = implode('.', array_reverse($fields));
565 121
        $path         = $mapping['name'];
566 121
        if ($propertyPath) {
567 19
            $path = $propertyPath . '.' . $path;
568
        }
569 121
        return [$path, $parent];
570
    }
571
572
    /**
573
     * Executes a query updating the given document.
574
     */
575 121
    private function executeQuery(object $document, array $newObj, array $options) : void
576
    {
577 121
        $className = get_class($document);
578 121
        $class     = $this->dm->getClassMetadata($className);
579 121
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
580 121
        $query     = ['_id' => $id];
581 121
        if ($class->isVersioned) {
582 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
583
        }
584 121
        $collection = $this->dm->getDocumentCollection($className);
585 121
        $result     = $collection->updateOne($query, $newObj, $options);
586 121
        if ($class->isVersioned && ! $result->getMatchedCount()) {
587 2
            throw LockException::lockFailed($document);
588
        }
589 119
    }
590
591
    /**
592
     * Remove from passed paths list all sub-paths.
593
     *
594
     * @param string[] $paths
595
     *
596
     * @return string[]
597
     */
598 117
    private function excludeSubPaths(array $paths) : array
599
    {
600 117
        if (empty($paths)) {
601 74
            return $paths;
602
        }
603 116
        sort($paths);
604 116
        $uniquePaths = [$paths[0]];
605 116
        for ($i = 1, $count = count($paths); $i < $count; ++$i) {
606 12
            if (strpos($paths[$i], end($uniquePaths)) === 0) {
607 10
                continue;
608
            }
609
610 4
            $uniquePaths[] = $paths[$i];
611
        }
612
613 116
        return $uniquePaths;
614
    }
615
}
616