Completed
Pull Request — master (#1880)
by
unknown
37:06 queued 09:40
created

CollectionPersister::pushAllCollections()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 17
cts 17
cp 1
rs 9.536
c 0
b 0
f 0
cc 4
nc 8
nop 5
crap 4
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_unique;
24
use function array_values;
25
use function count;
26
use function get_class;
27
use function implode;
28
use function spl_object_hash;
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
    /**
45
     * Validation map that is used for strategy validation in insertCollections method.
46
     */
47
    public const INSERT_STRATEGIES_MAP = [
48
        ClassMetadata::STORAGE_STRATEGY_PUSH_ALL => true,
49
        ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET => true,
50
    ];
51
52
    /**
53
     * The DocumentManager instance.
54
     *
55
     * @var DocumentManager
56
     */
57
    private $dm;
58
59
    /** @var PersistenceBuilder */
60
    private $pb;
61
62
    /** @var UnitOfWork */
63
    private $uow;
64
65 1125
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
66
    {
67 1125
        $this->dm  = $dm;
68 1125
        $this->pb  = $pb;
69 1125
        $this->uow = $uow;
70 1125
    }
71
72
    /**
73
     * Deletes a PersistentCollection instances completely from a document using $unset. If collections belong to the different
74
     *
75
     * @param PersistentCollectionInterface[] $collections
76
     * @param array                           $options
77
     */
78 35
    public function deleteAll(array $collections, array $options) : void
79
    {
80 35
        $parents       = [];
81 35
        $unsetPathsMap = [];
82
83 35
        foreach ($collections as $coll) {
84 35
            $mapping = $coll->getMapping();
85 35
            if ($mapping['isInverseSide']) {
86
                continue; // ignore inverse side
87
            }
88 35
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
89
                throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
90
            }
91 35
            [$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 mean $parents?

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...
92 35
            $oid                                = spl_object_hash($parent);
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
93 35
            $parents[$oid]                      = $parent;
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
94 35
            $unsetPathsMap[$oid][$propertyPath] = true;
95
        }
96
97 35
        foreach ($unsetPathsMap as $oid => $paths) {
98 35
            $unsetPaths = array_fill_keys($this->excludeSubPaths(array_keys($paths)), true);
99 35
            $query      = ['$unset' => $unsetPaths];
100 35
            $this->executeQuery($parents[$oid], $query, $options);
101
        }
102 35
    }
103
104
    /**
105
     * Deletes a PersistentCollection instance completely from a document using $unset.
106
     */
107 4
    public function delete(PersistentCollectionInterface $coll, array $options) : void
108
    {
109 4
        $mapping = $coll->getMapping();
110 4
        if ($mapping['isInverseSide']) {
111
            return; // ignore inverse side
112
        }
113 4
        if (CollectionHelper::isAtomic($mapping['strategy'])) {
114
            throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
115
        }
116 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...
117 4
        $query                   = ['$unset' => [$propertyPath => true]];
118 4
        $this->executeQuery($parent, $query, $options);
119 3
    }
120
121
    /**
122
     * Updates a PersistentCollection instance deleting removed rows and
123
     * inserting new rows.
124
     */
125 1
    public function update(PersistentCollectionInterface $coll, array $options) : void
126
    {
127 1
        $mapping = $coll->getMapping();
128
129 1
        if ($mapping['isInverseSide']) {
130
            return; // ignore inverse side
131
        }
132
133 1
        switch ($mapping['strategy']) {
134
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
135
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
136
                throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
137
138
            case ClassMetadata::STORAGE_STRATEGY_SET:
139
            case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
140
                $this->setCollection($coll, $options);
141
                break;
142
143
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
144
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
145 1
                $coll->initialize();
146 1
                $this->deleteElements($coll, $options);
147 1
                $this->insertElements($coll, $options);
148
                break;
149
150
            default:
151
                throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
152
        }
153
    }
154
155
    /**
156
     * Updates a list PersistentCollection instances deleting removed rows and inserting new rows.
157
     *
158
     * @param PersistentCollectionInterface[] $collections
159
     * @param array                           $options
160
     */
161 99
    public function updateAll(array $collections, array $options) : void
162
    {
163 99
        $setStrategyColls     = [];
164 99
        $addPushStrategyColls = [];
165
166 99
        foreach ($collections as $coll) {
167 99
            $mapping = $coll->getMapping();
168
169 99
            if ($mapping['isInverseSide']) {
170
                continue; // ignore inverse side
171
            }
172 99
            switch ($mapping['strategy']) {
173
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
174
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
175
                    throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
176
177
                case ClassMetadata::STORAGE_STRATEGY_SET:
178
                case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
179 10
                    $setStrategyColls[] = $coll;
180 10
                    break;
181
182
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
183
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
184 92
                    $addPushStrategyColls[] = $coll;
185 92
                    break;
186
187
                default:
188 99
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
189
            }
190
        }
191
192 99
        if (! empty($setStrategyColls)) {
193 10
            $this->setCollections($setStrategyColls, $options);
194
        }
195 99
        if (empty($addPushStrategyColls)) {
196 7
            return;
197
        }
198
199 92
        $this->deleteCollections($addPushStrategyColls, $options);
200 92
        $this->insertCollections($addPushStrategyColls, $options); // TODO
201 92
    }
202
203
    /**
204
     * Sets a PersistentCollection instance.
205
     *
206
     * This method is intended to be used with the "set" or "setArray"
207
     * strategies. The "setArray" strategy will ensure that the collection is
208
     * set as a BSON array, which means the collection elements will be
209
     * reindexed numerically before storage.
210
     */
211
    private function setCollection(PersistentCollectionInterface $coll, array $options) : void
212
    {
213
        [$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...
214
        $coll->initialize();
215
        $mapping = $coll->getMapping();
216
        $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy']));
217
        $query   = ['$set' => [$propertyPath => $setData]];
218
        $this->executeQuery($parent, $query, $options);
219
    }
220
221
    /**
222
     * Sets a list of PersistentCollection instances.
223
     *
224
     * This method is intended to be used with the "set" or "setArray"
225
     * strategies. The "setArray" strategy will ensure that the collection is
226
     * set as a BSON array, which means the collection elements will be
227
     * reindexed numerically before storage.
228
     *
229
     * @param PersistentCollectionInterface[] $collections
230
     * @param array                           $options
231
     */
232 10
    private function setCollections(array $collections, array $options) : void
233
    {
234 10
        $parents     = [];
235 10
        $pathCollMap = [];
236 10
        $pathsMap    = [];
237 10
        foreach ($collections as $coll) {
238 10
            [$propertyPath, $parent]          = $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...
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
239 10
            $oid                              = spl_object_hash($parent);
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
240 10
            $parents[$oid]                    = $parent;
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
241 10
            $pathCollMap[$oid][$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...
242 10
            $pathsMap[$oid][]                 = $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...
243
        }
244
245 10
        foreach ($pathsMap as $oid => $paths) {
246 10
            $paths = $this->excludeSubPaths($paths);
247
            /** @var PersistentCollectionInterface[] $setColls */
248 10
            $setColls   = array_intersect_key($pathCollMap[$oid], array_flip($paths));
249 10
            $setPayload = [];
250 10
            foreach ($setColls as $propertyPath => $coll) {
251 10
                $coll->initialize();
252 10
                $mapping                   = $coll->getMapping();
253 10
                $setData                   = $this->pb->prepareAssociatedCollectionValue(
254 10
                    $coll,
255 10
                    CollectionHelper::usesSet($mapping['strategy'])
256
                );
257 10
                $setPayload[$propertyPath] = $setData;
258
            }
259 10
            if (empty($setPayload)) {
260
                continue;
261
            }
262
263 10
            $query = ['$set' => $setPayload];
264 10
            $this->executeQuery($parents[$oid], $query, $options);
265
        }
266 10
    }
267
268
    /**
269
     * Deletes removed elements from a PersistentCollection instance.
270
     *
271
     * This method is intended to be used with the "pushAll" and "addToSet"
272
     * strategies.
273
     */
274 1
    private function deleteElements(PersistentCollectionInterface $coll, array $options) : void
275
    {
276 1
        $deleteDiff = $coll->getDeleteDiff();
277
278 1
        if (empty($deleteDiff)) {
279 1
            return;
280
        }
281
282
        [$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...
283
284
        $query = ['$unset' => []];
285
286
        foreach ($deleteDiff as $key => $document) {
287
            $query['$unset'][$propertyPath . '.' . $key] = true;
288
        }
289
290
        $this->executeQuery($parent, $query, $options);
291
292
        /**
293
         * @todo This is a hack right now because we don't have a proper way to
294
         * remove an element from an array by its key. Unsetting the key results
295
         * in the element being left in the array as null so we have to pull
296
         * null values.
297
         */
298
        $this->executeQuery($parent, ['$pull' => [$propertyPath => null]], $options);
299
    }
300
301
    /**
302
     * Deletes removed elements from a list of PersistentCollection instances.
303
     *
304
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
305
     *
306
     * @param PersistentCollectionInterface[] $collections
307
     * @param array                           $options
308
     */
309 92
    private function deleteCollections(array $collections, array $options) : void
310
    {
311 92
        $parents       = [];
312 92
        $pathCollMap   = [];
313 92
        $pathsMap      = [];
314 92
        $deleteDiffMap = [];
315
316 92
        foreach ($collections as $coll) {
317 92
            $coll->initialize();
318 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
319 2
                continue;
320
            }
321 90
            $deleteDiff = $coll->getDeleteDiff();
322
323 90
            if (empty($deleteDiff)) {
324 70
                continue;
325
            }
326 29
            [$propertyPath, $parent] = $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...
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
327
328 29
            $oid                                = spl_object_hash($parent);
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
329 29
            $parents[$oid]                      = $parent;
0 ignored issues
show
Bug introduced by
The variable $parent does not exist. Did you mean $parents?

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...
330 29
            $pathCollMap[$oid][$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...
331 29
            $pathsMap[$oid][]                   = $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...
332 29
            $deleteDiffMap[$oid][$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...
333
        }
334
335 92
        foreach ($pathsMap as $oid => $paths) {
336 29
            $paths        = $this->excludeSubPaths($paths);
337 29
            $deleteColls  = array_intersect_key($pathCollMap[$oid], array_flip($paths));
338 29
            $unsetPayload = [];
339 29
            $pullPayload  = [];
340 29
            foreach ($deleteColls as $propertyPath => $coll) {
341 29
                $deleteDiff = $deleteDiffMap[$oid][$propertyPath];
342 29
                foreach ($deleteDiff as $key => $document) {
343 29
                    $unsetPayload[$propertyPath . '.' . $key] = true;
344
                }
345 29
                $pullPayload[$propertyPath] = null;
346
            }
347
348 29
            if (! empty($unsetPayload)) {
349 29
                $this->executeQuery($parents[$oid], ['$unset' => $unsetPayload], $options);
350
            }
351 29
            if (empty($pullPayload)) {
352
                continue;
353
            }
354
355
            /**
356
             * @todo This is a hack right now because we don't have a proper way to
357
             * remove an element from an array by its key. Unsetting the key results
358
             * in the element being left in the array as null so we have to pull
359
             * null values.
360
             */
361 29
            $this->executeQuery($parents[$oid], ['$pull' => $pullPayload], $options);
362
        }
363 92
    }
364
365
    /**
366
     * Inserts new elements for a PersistentCollection instance.
367
     *
368
     * This method is intended to be used with the "pushAll" and "addToSet"
369
     * strategies.
370
     */
371 1
    private function insertElements(PersistentCollectionInterface $coll, array $options) : void
372
    {
373 1
        $insertDiff = $coll->getInsertDiff();
374
375 1
        if (empty($insertDiff)) {
376
            return;
377
        }
378
379 1
        $mapping = $coll->getMapping();
380
381 1
        switch ($mapping['strategy']) {
382
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
383 1
                $operator = 'push';
384 1
                break;
385
386
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
387
                $operator = 'addToSet';
388
                break;
389
390
            default:
391
                throw new LogicException(sprintf('Invalid strategy %s given for insertElements', $mapping['strategy']));
392
        }
393
394 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...
395
396 1
        $callback = isset($mapping['embedded'])
397
            ? function ($v) use ($mapping) {
398 1
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
399 1
            }
400
            : function ($v) use ($mapping) {
401
                return $this->pb->prepareReferencedDocumentValue($mapping, $v);
402 1
            };
403
404 1
        $value = array_values(array_map($callback, $insertDiff));
405
406 1
        $query = ['$' . $operator => [$propertyPath => ['$each' => $value]]];
407
408 1
        $this->executeQuery($parent, $query, $options);
409
    }
410
411
    /**
412
     * Inserts new elements for a list of PersistentCollection instances.
413
     *
414
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
415
     *
416
     * @param PersistentCollectionInterface[] $collections
417
     * @param array                           $options
418
     */
419 92
    private function insertCollections(array $collections, array $options) : void
420
    {
421 92
        $parents             = [];
422 92
        $pushAllPathCollMap  = [];
423 92
        $addToSetPathCollMap = [];
424 92
        $pushAllPathsMap     = [];
425 92
        $addToSetPathsMap    = [];
426 92
        $diffsMap            = [];
427
428 92
        foreach ($collections as $coll) {
429 92
            $coll->initialize();
430 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
431 2
                continue;
432
            }
433 90
            $insertDiff = $coll->getInsertDiff();
434
435 90
            if (empty($insertDiff)) {
436 21
                continue;
437
            }
438
439 75
            $mapping  = $coll->getMapping();
440 75
            $strategy = $mapping['strategy'];
441
442 75
            if (empty(self::INSERT_STRATEGIES_MAP[$strategy])) {
443
                throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
444
            }
445
446 75
            [$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 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...
447 75
            $oid                           = spl_object_hash($parent);
0 ignored issues
show
Bug introduced by
The variable $parent 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...
448 75
            $parents[$oid]                 = $parent;
0 ignored issues
show
Bug introduced by
The variable $parent 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...
449 75
            $diffsMap[$oid][$propertyPath] = $insertDiff;
450
451 75
            switch ($strategy) {
452
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
453 71
                    $pushAllPathCollMap[$oid][$propertyPath] = $coll;
454 71
                    $pushAllPathsMap[$oid][]                 = $propertyPath;
455 71
                    break;
456
457
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
458 6
                    $addToSetPathCollMap[$oid][$propertyPath] = $coll;
459 6
                    $addToSetPathsMap[$oid][]                 = $propertyPath;
460 6
                    break;
461
462
                default:
463 75
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
464
            }
465
        }
466
467 92
        foreach ($parents as $oid => $parent) {
468 75
            if (! empty($pushAllPathsMap[$oid])) {
469 71
                $this->pushAllCollections(
470 71
                    $parent,
471 71
                    $pushAllPathsMap[$oid],
472 71
                    $pushAllPathCollMap[$oid],
473 71
                    $diffsMap[$oid],
474 71
                    $options
475
                );
476
            }
477 75
            if (empty($addToSetPathsMap[$oid])) {
478 69
                continue;
479
            }
480
481 6
            $this->addToSetCollections(
482 6
                $parent,
483 6
                $addToSetPathsMap[$oid],
484 6
                $addToSetPathCollMap[$oid],
485 6
                $diffsMap[$oid],
486 6
                $options
487
            );
488
        }
489 92
    }
490
491
    /**
492
     * Perform collections update for 'pushAll' strategy.
493
     *
494
     * @param object $parent       Parent object to which passed collections is belong.
495
     * @param array  $collsPaths   Paths of collections that is passed.
496
     * @param array  $pathCollsMap List of collections indexed by their paths.
497
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
498
     * @param array  $options
499
     */
500 71
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
501
    {
502 71
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
503
        /** @var PersistentCollectionInterface[] $pushAllColls */
504 71
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
505 71
        $pushAllPayload = [];
506 71
        foreach ($pushAllColls as $propertyPath => $coll) {
507 71
            $callback                      = $this->getValuePrepareCallback($coll);
508 71
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
509 71
            $pushAllPayload[$propertyPath] = ['$each' => $value];
510
        }
511
512 71
        if (! empty($pushAllPayload)) {
513 71
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
514
        }
515
516 71
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
517 71
        foreach ($pushAllColls as $propertyPath => $coll) {
518 1
            $callback = $this->getValuePrepareCallback($coll);
519 1
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
520 1
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
521 1
            $this->executeQuery($parent, $query, $options);
522
        }
523 71
    }
524
525
    /**
526
     * Perform collections update by 'addToSet' strategy.
527
     *
528
     * @param object $parent       Parent object to which passed collections is belong.
529
     * @param array  $collsPaths   Paths of collections that is passed.
530
     * @param array  $pathCollsMap List of collections indexed by their paths.
531
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
532
     * @param array  $options
533
     */
534 6
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
535
    {
536 6
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
537
        /** @var PersistentCollectionInterface[] $addToSetColls */
538 6
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
539
540 6
        $addToSetPayload = [];
541 6
        foreach ($addToSetColls as $propertyPath => $coll) {
542 6
            $callback                       = $this->getValuePrepareCallback($coll);
543 6
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
544 6
            $addToSetPayload[$propertyPath] = ['$each' => $value];
545
        }
546
547 6
        if (empty($addToSetPayload)) {
548
            return;
549
        }
550
551 6
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
552 6
    }
553
554
    /**
555
     * Return callback instance for specified collection. This callback will prepare values for query from documents
556
     * that collection contain.
557
     */
558 75
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
559
    {
560 75
        $mapping = $coll->getMapping();
561 75
        if (isset($mapping['embedded'])) {
562
            return function ($v) use ($mapping) {
563 39
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
564 39
            };
565
        }
566
567
        return function ($v) use ($mapping) {
568 37
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
569 37
        };
570
    }
571
572
    /**
573
     * Gets the parent information for a given PersistentCollection. It will
574
     * retrieve the top-level persistent Document that the PersistentCollection
575
     * lives in. We can use this to issue queries when updating a
576
     * PersistentCollection that is multiple levels deep inside an embedded
577
     * document.
578
     *
579
     *     <code>
580
     *     list($path, $parent) = $this->getPathAndParent($coll)
581
     *     </code>
582
     */
583 115
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
584
    {
585 115
        $mapping = $coll->getMapping();
586 115
        $fields  = [];
587 115
        $parent  = $coll->getOwner();
588 115
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
589 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...
590 18
            if (isset($m['reference'])) {
591
                break;
592
            }
593 18
            $parent   = $owner;
594 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...
595
        }
596 115
        $propertyPath = implode('.', array_reverse($fields));
597 115
        $path         = $mapping['name'];
598 115
        if ($propertyPath) {
599 18
            $path = $propertyPath . '.' . $path;
600
        }
601 115
        return [$path, $parent];
602
    }
603
604
    /**
605
     * Executes a query updating the given document.
606
     */
607 115
    private function executeQuery(object $document, array $newObj, array $options) : void
608
    {
609 115
        $className = get_class($document);
610 115
        $class     = $this->dm->getClassMetadata($className);
611 115
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
612 115
        $query     = ['_id' => $id];
613 115
        if ($class->isVersioned) {
614 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
615
        }
616 115
        $collection = $this->dm->getDocumentCollection($className);
617 115
        $result     = $collection->updateOne($query, $newObj, $options);
618 115
        if ($class->isVersioned && ! $result->getMatchedCount()) {
619 2
            throw LockException::lockFailed($document);
620
        }
621 113
    }
622
623
    /**
624
     * Remove from passed paths list all sub-paths.
625
     *
626
     * @param string[] $paths
627
     *
628
     * @return string[]
629
     */
630 110
    private function excludeSubPaths(array $paths) : array
631
    {
632 110
        $checkedPaths = [];
633 110
        $pathsAmount  = count($paths);
634 110
        $paths        = array_unique($paths);
635 110
        for ($i = 0; $i < $pathsAmount; $i++) {
636 110
            $isSubPath = false;
637 110
            $j         = 0;
638 110
            for (; $j < $pathsAmount; $j++) {
639 110
                if ($i !== $j && strpos($paths[$i], $paths[$j]) === 0) {
640 4
                    $isSubPath = true;
641 4
                    break;
642
                }
643
            }
644 110
            if ($isSubPath) {
645 4
                continue;
646
            }
647
648 110
            $checkedPaths[] = $paths[$i];
649
        }
650
651 110
        return $checkedPaths;
652
    }
653
}
654