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

CollectionPersister::update()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 16

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 7
cts 14
cp 0.5
rs 8.2114
c 0
b 0
f 0
cc 8
nc 8
nop 2
crap 16
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 1124
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
54
    {
55 1124
        $this->dm  = $dm;
56 1124
        $this->pb  = $pb;
57 1124
        $this->uow = $uow;
58 1124
    }
59
60
    /**
61
     * Deletes a PersistentCollection instances completely from a document using $unset.
62
     *
63
     * @param PersistentCollectionInterface[] $collections
64
     * @param array                           $options
65
     */
66 35
    public function deleteAll(object $parent, array $collections, array $options) : void
67
    {
68 35
        $unsetPathsMap = [];
69
70 35
        foreach ($collections as $collection) {
71 35
            $mapping = $collection->getMapping();
72 35
            if ($mapping['isInverseSide']) {
73
                continue; // ignore inverse side
74
            }
75 35
            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 35
            [$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 35
            $unsetPathsMap[$propertyPath] = true;
80
        }
81
82 35
        if (empty($unsetPathsMap)) {
83
            return;
84
        }
85
86 35
        $unsetPaths = array_fill_keys($this->excludeSubPaths(array_keys($unsetPathsMap)), true);
87 35
        $query      = ['$unset' => $unsetPaths];
88 35
        $this->executeQuery($parent, $query, $options);
89 35
    }
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 99
    public function updateAll(object $parent, array $collections, array $options) : void
149
    {
150 99
        $setStrategyColls     = [];
151 99
        $addPushStrategyColls = [];
152
153 99
        foreach ($collections as $coll) {
154 99
            $mapping = $coll->getMapping();
155
156 99
            if ($mapping['isInverseSide']) {
157
                continue; // ignore inverse side
158
            }
159 99
            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 10
                    $setStrategyColls[] = $coll;
167 10
                    break;
168
169
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
170
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
171 92
                    $addPushStrategyColls[] = $coll;
172 92
                    break;
173
174
                default:
175 99
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
176
            }
177
        }
178
179 99
        if (! empty($setStrategyColls)) {
180 10
            $this->setCollections($parent, $setStrategyColls, $options);
181
        }
182 99
        if (empty($addPushStrategyColls)) {
183 7
            return;
184
        }
185
186 92
        $this->deleteCollections($parent, $addPushStrategyColls, $options);
187 92
        $this->insertCollections($parent, $addPushStrategyColls, $options);
188 92
    }
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 10
    private function setCollections(object $parent, array $collections, array $options) : void
220
    {
221 10
        $pathCollMap = [];
222 10
        $paths       = [];
223 10
        foreach ($collections as $coll) {
224 10
            [$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 10
            $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 10
            $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 10
        $paths = $this->excludeSubPaths($paths);
230
        /** @var PersistentCollectionInterface[] $setColls */
231 10
        $setColls   = array_intersect_key($pathCollMap, array_flip($paths));
232 10
        $setPayload = [];
233 10
        foreach ($setColls as $propertyPath => $coll) {
234 10
            $coll->initialize();
235 10
            $mapping                   = $coll->getMapping();
236 10
            $setData                   = $this->pb->prepareAssociatedCollectionValue(
237 10
                $coll,
238 10
                CollectionHelper::usesSet($mapping['strategy'])
239
            );
240 10
            $setPayload[$propertyPath] = $setData;
241
        }
242 10
        if (empty($setPayload)) {
243
            return;
244
        }
245
246 10
        $query = ['$set' => $setPayload];
247 10
        $this->executeQuery($parent, $query, $options);
248 10
    }
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 92
    private function deleteCollections(object $parent, array $collections, array $options) : void
292
    {
293 92
        $pathCollMap   = [];
294 92
        $paths         = [];
295 92
        $deleteDiffMap = [];
296
297 92
        foreach ($collections as $coll) {
298 92
            $coll->initialize();
299 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
300 2
                continue;
301
            }
302 90
            $deleteDiff = $coll->getDeleteDiff();
303
304 90
            if (empty($deleteDiff)) {
305 70
                continue;
306
            }
307 29
            [$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 29
            $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 29
            $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 29
            $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 92
        $paths        = $this->excludeSubPaths($paths);
315 92
        $deleteColls  = array_intersect_key($pathCollMap, array_flip($paths));
316 92
        $unsetPayload = [];
317 92
        $pullPayload  = [];
318 92
        foreach ($deleteColls as $propertyPath => $coll) {
319 29
            $deleteDiff = $deleteDiffMap[$propertyPath];
320 29
            foreach ($deleteDiff as $key => $document) {
321 29
                $unsetPayload[$propertyPath . '.' . $key] = true;
322
            }
323 29
            $pullPayload[$propertyPath] = null;
324
        }
325
326 92
        if (! empty($unsetPayload)) {
327 29
            $this->executeQuery($parent, ['$unset' => $unsetPayload], $options);
328
        }
329 92
        if (empty($pullPayload)) {
330 72
            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 29
        $this->executeQuery($parent, ['$pull' => $pullPayload], $options);
340 29
    }
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 92
    private function insertCollections(object $parent, array $collections, array $options) : void
397
    {
398 92
        $pushAllPathCollMap  = [];
399 92
        $addToSetPathCollMap = [];
400 92
        $pushAllPaths        = [];
401 92
        $addToSetPaths       = [];
402 92
        $diffsMap            = [];
403
404 92
        foreach ($collections as $coll) {
405 92
            $coll->initialize();
406 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
407 2
                continue;
408
            }
409 90
            $insertDiff = $coll->getInsertDiff();
410
411 90
            if (empty($insertDiff)) {
412 21
                continue;
413
            }
414
415 75
            $mapping  = $coll->getMapping();
416 75
            $strategy = $mapping['strategy'];
417
418 75
            [$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 75
            $diffsMap[$propertyPath] = $insertDiff;
420
421 75
            switch ($strategy) {
422
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
423 71
                    $pushAllPathCollMap[$propertyPath] = $coll;
424 71
                    $pushAllPaths[]                    = $propertyPath;
425 71
                    break;
426
427
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
428 6
                    $addToSetPathCollMap[$propertyPath] = $coll;
429 6
                    $addToSetPaths[]                    = $propertyPath;
430 6
                    break;
431
432
                default:
433 75
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
434
            }
435
        }
436
437 92
        if (! empty($pushAllPaths)) {
438 71
            $this->pushAllCollections(
439 71
                $parent,
440 71
                $pushAllPaths,
441 71
                $pushAllPathCollMap,
442 71
                $diffsMap,
443 71
                $options
444
            );
445
        }
446 92
        if (empty($addToSetPaths)) {
447 87
            return;
448
        }
449
450 6
        $this->addToSetCollections(
451 6
            $parent,
452 6
            $addToSetPaths,
453 6
            $addToSetPathCollMap,
454 6
            $diffsMap,
455 6
            $options
456
        );
457 6
    }
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 71
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
469
    {
470 71
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
471
        /** @var PersistentCollectionInterface[] $pushAllColls */
472 71
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
473 71
        $pushAllPayload = [];
474 71
        foreach ($pushAllColls as $propertyPath => $coll) {
475 71
            $callback                      = $this->getValuePrepareCallback($coll);
476 71
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
477 71
            $pushAllPayload[$propertyPath] = ['$each' => $value];
478
        }
479
480 71
        if (! empty($pushAllPayload)) {
481 71
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
482
        }
483
484 71
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
485 71
        foreach ($pushAllColls as $propertyPath => $coll) {
486 1
            $callback = $this->getValuePrepareCallback($coll);
487 1
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
488 1
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
489 1
            $this->executeQuery($parent, $query, $options);
490
        }
491 71
    }
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 6
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
503
    {
504 6
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
505
        /** @var PersistentCollectionInterface[] $addToSetColls */
506 6
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
507
508 6
        $addToSetPayload = [];
509 6
        foreach ($addToSetColls as $propertyPath => $coll) {
510 6
            $callback                       = $this->getValuePrepareCallback($coll);
511 6
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
512 6
            $addToSetPayload[$propertyPath] = ['$each' => $value];
513
        }
514
515 6
        if (empty($addToSetPayload)) {
516
            return;
517
        }
518
519 6
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
520 6
    }
521
522
    /**
523
     * Return callback instance for specified collection. This callback will prepare values for query from documents
524
     * that collection contain.
525
     */
526 75
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
527
    {
528 75
        $mapping = $coll->getMapping();
529 75
        if (isset($mapping['embedded'])) {
530
            return function ($v) use ($mapping) {
531 39
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
532 39
            };
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 115
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
552
    {
553 115
        $mapping = $coll->getMapping();
554 115
        $fields  = [];
555 115
        $parent  = $coll->getOwner();
556 115
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
557 18
            [$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 18
            if (isset($m['reference'])) {
559
                break;
560
            }
561 18
            $parent   = $owner;
562 18
            $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 115
        $propertyPath = implode('.', array_reverse($fields));
565 115
        $path         = $mapping['name'];
566 115
        if ($propertyPath) {
567 18
            $path = $propertyPath . '.' . $path;
568
        }
569 115
        return [$path, $parent];
570
    }
571
572
    /**
573
     * Executes a query updating the given document.
574
     */
575 115
    private function executeQuery(object $document, array $newObj, array $options) : void
576
    {
577 115
        $className = get_class($document);
578 115
        $class     = $this->dm->getClassMetadata($className);
579 115
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
580 115
        $query     = ['_id' => $id];
581 115
        if ($class->isVersioned) {
582 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
583
        }
584 115
        $collection = $this->dm->getDocumentCollection($className);
585 115
        $result     = $collection->updateOne($query, $newObj, $options);
586 115
        if ($class->isVersioned && ! $result->getMatchedCount()) {
587 2
            throw LockException::lockFailed($document);
588
        }
589 113
    }
590
591
    /**
592
     * Remove from passed paths list all sub-paths.
593
     *
594
     * @param string[] $paths
595
     *
596
     * @return string[]
597
     */
598 111
    private function excludeSubPaths(array $paths) : array
599
    {
600 111
        if (empty($paths)) {
601 72
            return $paths;
602
        }
603 110
        sort($paths);
604 110
        $uniquePaths = [$paths[0]];
605 110
        for ($i = 1, $count = count($paths); $i < $count; ++$i) {
606 6
            if (strpos($paths[$i], end($uniquePaths)) === 0) {
607 4
                continue;
608
            }
609
610 4
            $uniquePaths[] = $paths[$i];
611
        }
612
613 110
        return $uniquePaths;
614
    }
615
}
616