Completed
Pull Request — master (#1880)
by
unknown
33:57
created

CollectionPersister   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 606
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 86.11%

Importance

Changes 0
Metric Value
wmc 82
lcom 1
cbo 6
dl 0
loc 606
ccs 248
cts 288
cp 0.8611
rs 1.994
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A deleteAll() 0 25 5
A delete() 0 13 3
B update() 0 29 8
B updateAll() 0 41 11
A setCollection() 0 9 1
A setCollections() 0 35 5
A deleteElements() 0 26 3
B deleteCollections() 0 55 9
B insertElements() 0 39 5
C insertCollections() 0 71 10
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 spl_object_hash;
30
use function sprintf;
31
use function strpos;
32
33
/**
34
 * The CollectionPersister is responsible for persisting collections of embedded
35
 * or referenced documents. When a PersistentCollection is scheduledForDeletion
36
 * in the UnitOfWork by calling PersistentCollection::clear() or is
37
 * de-referenced in the domain application code, CollectionPersister::delete()
38
 * will be called. When documents within the PersistentCollection are added or
39
 * removed, CollectionPersister::update() will be called, which may set the
40
 * entire collection or delete/insert individual elements, depending on the
41
 * mapping strategy.
42
 */
43
class CollectionPersister
44
{
45
    /**
46
     * Validation map that is used for strategy validation in insertCollections method.
47
     */
48
    public const INSERT_STRATEGIES_MAP = [
49
        ClassMetadata::STORAGE_STRATEGY_PUSH_ALL => true,
50
        ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET => true,
51
    ];
52
53
    /**
54
     * The DocumentManager instance.
55
     *
56
     * @var DocumentManager
57
     */
58
    private $dm;
59
60
    /** @var PersistenceBuilder */
61
    private $pb;
62
63
    /** @var UnitOfWork */
64
    private $uow;
65
66 1125
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
67
    {
68 1125
        $this->dm  = $dm;
69 1125
        $this->pb  = $pb;
70 1125
        $this->uow = $uow;
71 1125
    }
72
73
    /**
74
     * Deletes a PersistentCollection instances completely from a document using $unset. If collections belong to the different
75
     *
76
     * @param PersistentCollectionInterface[] $collections
77
     * @param array                           $options
78
     */
79 35
    public function deleteAll(array $collections, array $options) : void
80
    {
81 35
        $parents       = [];
82 35
        $unsetPathsMap = [];
83
84 35
        foreach ($collections as $coll) {
85 35
            $mapping = $coll->getMapping();
86 35
            if ($mapping['isInverseSide']) {
87
                continue; // ignore inverse side
88
            }
89 35
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
90
                throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
91
            }
92 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...
93 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...
94 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...
95 35
            $unsetPathsMap[$oid][$propertyPath] = true;
96
        }
97
98 35
        foreach ($unsetPathsMap as $oid => $paths) {
99 35
            $unsetPaths = array_fill_keys($this->excludeSubPaths(array_keys($paths)), true);
100 35
            $query      = ['$unset' => $unsetPaths];
101 35
            $this->executeQuery($parents[$oid], $query, $options);
102
        }
103 35
    }
104
105
    /**
106
     * Deletes a PersistentCollection instance completely from a document using $unset.
107
     */
108 4
    public function delete(PersistentCollectionInterface $coll, array $options) : void
109
    {
110 4
        $mapping = $coll->getMapping();
111 4
        if ($mapping['isInverseSide']) {
112
            return; // ignore inverse side
113
        }
114 4
        if (CollectionHelper::isAtomic($mapping['strategy'])) {
115
            throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
116
        }
117 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...
118 4
        $query                   = ['$unset' => [$propertyPath => true]];
119 4
        $this->executeQuery($parent, $query, $options);
120 3
    }
121
122
    /**
123
     * Updates a PersistentCollection instance deleting removed rows and
124
     * inserting new rows.
125
     */
126 1
    public function update(PersistentCollectionInterface $coll, array $options) : void
127
    {
128 1
        $mapping = $coll->getMapping();
129
130 1
        if ($mapping['isInverseSide']) {
131
            return; // ignore inverse side
132
        }
133
134 1
        switch ($mapping['strategy']) {
135
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
136
            case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
137
                throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
138
139
            case ClassMetadata::STORAGE_STRATEGY_SET:
140
            case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
141
                $this->setCollection($coll, $options);
142
                break;
143
144
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
145
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
146 1
                $coll->initialize();
147 1
                $this->deleteElements($coll, $options);
148 1
                $this->insertElements($coll, $options);
149
                break;
150
151
            default:
152
                throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
153
        }
154
    }
155
156
    /**
157
     * Updates a list PersistentCollection instances deleting removed rows and inserting new rows.
158
     *
159
     * @param PersistentCollectionInterface[] $collections
160
     * @param array                           $options
161
     */
162 99
    public function updateAll(array $collections, array $options) : void
163
    {
164 99
        $setStrategyColls     = [];
165 99
        $addPushStrategyColls = [];
166
167 99
        foreach ($collections as $coll) {
168 99
            $mapping = $coll->getMapping();
169
170 99
            if ($mapping['isInverseSide']) {
171
                continue; // ignore inverse side
172
            }
173 99
            switch ($mapping['strategy']) {
174
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
175
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
176
                    throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
177
178
                case ClassMetadata::STORAGE_STRATEGY_SET:
179
                case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
180 10
                    $setStrategyColls[] = $coll;
181 10
                    break;
182
183
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
184
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
185 92
                    $addPushStrategyColls[] = $coll;
186 92
                    break;
187
188
                default:
189 99
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
190
            }
191
        }
192
193 99
        if (! empty($setStrategyColls)) {
194 10
            $this->setCollections($setStrategyColls, $options);
195
        }
196 99
        if (empty($addPushStrategyColls)) {
197 7
            return;
198
        }
199
200 92
        $this->deleteCollections($addPushStrategyColls, $options);
201 92
        $this->insertCollections($addPushStrategyColls, $options); // TODO
202 92
    }
203
204
    /**
205
     * Sets a PersistentCollection instance.
206
     *
207
     * This method is intended to be used with the "set" or "setArray"
208
     * strategies. The "setArray" strategy will ensure that the collection is
209
     * set as a BSON array, which means the collection elements will be
210
     * reindexed numerically before storage.
211
     */
212
    private function setCollection(PersistentCollectionInterface $coll, array $options) : void
213
    {
214
        [$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...
215
        $coll->initialize();
216
        $mapping = $coll->getMapping();
217
        $setData = $this->pb->prepareAssociatedCollectionValue($coll, CollectionHelper::usesSet($mapping['strategy']));
218
        $query   = ['$set' => [$propertyPath => $setData]];
219
        $this->executeQuery($parent, $query, $options);
220
    }
221
222
    /**
223
     * Sets a list of PersistentCollection instances.
224
     *
225
     * This method is intended to be used with the "set" or "setArray"
226
     * strategies. The "setArray" strategy will ensure that the collection is
227
     * set as a BSON array, which means the collection elements will be
228
     * reindexed numerically before storage.
229
     *
230
     * @param PersistentCollectionInterface[] $collections
231
     * @param array                           $options
232
     */
233 10
    private function setCollections(array $collections, array $options) : void
234
    {
235 10
        $parents     = [];
236 10
        $pathCollMap = [];
237 10
        $pathsMap    = [];
238 10
        foreach ($collections as $coll) {
239 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...
240 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...
241 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...
242 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...
243 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...
244
        }
245
246 10
        foreach ($pathsMap as $oid => $paths) {
247 10
            $paths = $this->excludeSubPaths($paths);
248
            /** @var PersistentCollectionInterface[] $setColls */
249 10
            $setColls   = array_intersect_key($pathCollMap[$oid], array_flip($paths));
250 10
            $setPayload = [];
251 10
            foreach ($setColls as $propertyPath => $coll) {
252 10
                $coll->initialize();
253 10
                $mapping                   = $coll->getMapping();
254 10
                $setData                   = $this->pb->prepareAssociatedCollectionValue(
255 10
                    $coll,
256 10
                    CollectionHelper::usesSet($mapping['strategy'])
257
                );
258 10
                $setPayload[$propertyPath] = $setData;
259
            }
260 10
            if (empty($setPayload)) {
261
                continue;
262
            }
263
264 10
            $query = ['$set' => $setPayload];
265 10
            $this->executeQuery($parents[$oid], $query, $options);
266
        }
267 10
    }
268
269
    /**
270
     * Deletes removed elements from a PersistentCollection instance.
271
     *
272
     * This method is intended to be used with the "pushAll" and "addToSet"
273
     * strategies.
274
     */
275 1
    private function deleteElements(PersistentCollectionInterface $coll, array $options) : void
276
    {
277 1
        $deleteDiff = $coll->getDeleteDiff();
278
279 1
        if (empty($deleteDiff)) {
280 1
            return;
281
        }
282
283
        [$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...
284
285
        $query = ['$unset' => []];
286
287
        foreach ($deleteDiff as $key => $document) {
288
            $query['$unset'][$propertyPath . '.' . $key] = true;
289
        }
290
291
        $this->executeQuery($parent, $query, $options);
292
293
        /**
294
         * @todo This is a hack right now because we don't have a proper way to
295
         * remove an element from an array by its key. Unsetting the key results
296
         * in the element being left in the array as null so we have to pull
297
         * null values.
298
         */
299
        $this->executeQuery($parent, ['$pull' => [$propertyPath => null]], $options);
300
    }
301
302
    /**
303
     * Deletes removed elements from a list of PersistentCollection instances.
304
     *
305
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
306
     *
307
     * @param PersistentCollectionInterface[] $collections
308
     * @param array                           $options
309
     */
310 92
    private function deleteCollections(array $collections, array $options) : void
311
    {
312 92
        $parents       = [];
313 92
        $pathCollMap   = [];
314 92
        $pathsMap      = [];
315 92
        $deleteDiffMap = [];
316
317 92
        foreach ($collections as $coll) {
318 92
            $coll->initialize();
319 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
320 2
                continue;
321
            }
322 90
            $deleteDiff = $coll->getDeleteDiff();
323
324 90
            if (empty($deleteDiff)) {
325 70
                continue;
326
            }
327 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...
328
329 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...
330 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...
331 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...
332 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...
333 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...
334
        }
335
336 92
        foreach ($pathsMap as $oid => $paths) {
337 29
            $paths        = $this->excludeSubPaths($paths);
338 29
            $deleteColls  = array_intersect_key($pathCollMap[$oid], array_flip($paths));
339 29
            $unsetPayload = [];
340 29
            $pullPayload  = [];
341 29
            foreach ($deleteColls as $propertyPath => $coll) {
342 29
                $deleteDiff = $deleteDiffMap[$oid][$propertyPath];
343 29
                foreach ($deleteDiff as $key => $document) {
344 29
                    $unsetPayload[$propertyPath . '.' . $key] = true;
345
                }
346 29
                $pullPayload[$propertyPath] = null;
347
            }
348
349 29
            if (! empty($unsetPayload)) {
350 29
                $this->executeQuery($parents[$oid], ['$unset' => $unsetPayload], $options);
351
            }
352 29
            if (empty($pullPayload)) {
353
                continue;
354
            }
355
356
            /**
357
             * @todo This is a hack right now because we don't have a proper way to
358
             * remove an element from an array by its key. Unsetting the key results
359
             * in the element being left in the array as null so we have to pull
360
             * null values.
361
             */
362 29
            $this->executeQuery($parents[$oid], ['$pull' => $pullPayload], $options);
363
        }
364 92
    }
365
366
    /**
367
     * Inserts new elements for a PersistentCollection instance.
368
     *
369
     * This method is intended to be used with the "pushAll" and "addToSet"
370
     * strategies.
371
     */
372 1
    private function insertElements(PersistentCollectionInterface $coll, array $options) : void
373
    {
374 1
        $insertDiff = $coll->getInsertDiff();
375
376 1
        if (empty($insertDiff)) {
377
            return;
378
        }
379
380 1
        $mapping = $coll->getMapping();
381
382 1
        switch ($mapping['strategy']) {
383
            case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
384 1
                $operator = 'push';
385 1
                break;
386
387
            case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
388
                $operator = 'addToSet';
389
                break;
390
391
            default:
392
                throw new LogicException(sprintf('Invalid strategy %s given for insertElements', $mapping['strategy']));
393
        }
394
395 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...
396
397 1
        $callback = isset($mapping['embedded'])
398
            ? function ($v) use ($mapping) {
399 1
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
400 1
            }
401
            : function ($v) use ($mapping) {
402
                return $this->pb->prepareReferencedDocumentValue($mapping, $v);
403 1
            };
404
405 1
        $value = array_values(array_map($callback, $insertDiff));
406
407 1
        $query = ['$' . $operator => [$propertyPath => ['$each' => $value]]];
408
409 1
        $this->executeQuery($parent, $query, $options);
410
    }
411
412
    /**
413
     * Inserts new elements for a list of PersistentCollection instances.
414
     *
415
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
416
     *
417
     * @param PersistentCollectionInterface[] $collections
418
     * @param array                           $options
419
     */
420 92
    private function insertCollections(array $collections, array $options) : void
421
    {
422 92
        $parents             = [];
423 92
        $pushAllPathCollMap  = [];
424 92
        $addToSetPathCollMap = [];
425 92
        $pushAllPathsMap     = [];
426 92
        $addToSetPathsMap    = [];
427 92
        $diffsMap            = [];
428
429 92
        foreach ($collections as $coll) {
430 92
            $coll->initialize();
431 92
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
432 2
                continue;
433
            }
434 90
            $insertDiff = $coll->getInsertDiff();
435
436 90
            if (empty($insertDiff)) {
437 21
                continue;
438
            }
439
440 75
            $mapping  = $coll->getMapping();
441 75
            $strategy = $mapping['strategy'];
442
443 75
            if (empty(self::INSERT_STRATEGIES_MAP[$strategy])) {
444
                throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
445
            }
446
447 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...
448 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...
449 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...
450 75
            $diffsMap[$oid][$propertyPath] = $insertDiff;
451
452 75
            switch ($strategy) {
453
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
454 71
                    $pushAllPathCollMap[$oid][$propertyPath] = $coll;
455 71
                    $pushAllPathsMap[$oid][]                 = $propertyPath;
456 71
                    break;
457
458
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
459 6
                    $addToSetPathCollMap[$oid][$propertyPath] = $coll;
460 6
                    $addToSetPathsMap[$oid][]                 = $propertyPath;
461 6
                    break;
462
463
                default:
464 75
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
465
            }
466
        }
467
468 92
        foreach ($parents as $oid => $parent) {
469 75
            if (! empty($pushAllPathsMap[$oid])) {
470 71
                $this->pushAllCollections(
471 71
                    $parent,
472 71
                    $pushAllPathsMap[$oid],
473 71
                    $pushAllPathCollMap[$oid],
474 71
                    $diffsMap[$oid],
475 71
                    $options
476
                );
477
            }
478 75
            if (empty($addToSetPathsMap[$oid])) {
479 69
                continue;
480
            }
481
482 6
            $this->addToSetCollections(
483 6
                $parent,
484 6
                $addToSetPathsMap[$oid],
485 6
                $addToSetPathCollMap[$oid],
486 6
                $diffsMap[$oid],
487 6
                $options
488
            );
489
        }
490 92
    }
491
492
    /**
493
     * Perform collections update for 'pushAll' strategy.
494
     *
495
     * @param object $parent       Parent object to which passed collections is belong.
496
     * @param array  $collsPaths   Paths of collections that is passed.
497
     * @param array  $pathCollsMap List of collections indexed by their paths.
498
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
499
     * @param array  $options
500
     */
501 71
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
502
    {
503 71
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
504
        /** @var PersistentCollectionInterface[] $pushAllColls */
505 71
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
506 71
        $pushAllPayload = [];
507 71
        foreach ($pushAllColls as $propertyPath => $coll) {
508 71
            $callback                      = $this->getValuePrepareCallback($coll);
509 71
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
510 71
            $pushAllPayload[$propertyPath] = ['$each' => $value];
511
        }
512
513 71
        if (! empty($pushAllPayload)) {
514 71
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
515
        }
516
517 71
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
518 71
        foreach ($pushAllColls as $propertyPath => $coll) {
519 1
            $callback = $this->getValuePrepareCallback($coll);
520 1
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
521 1
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
522 1
            $this->executeQuery($parent, $query, $options);
523
        }
524 71
    }
525
526
    /**
527
     * Perform collections update by 'addToSet' strategy.
528
     *
529
     * @param object $parent       Parent object to which passed collections is belong.
530
     * @param array  $collsPaths   Paths of collections that is passed.
531
     * @param array  $pathCollsMap List of collections indexed by their paths.
532
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
533
     * @param array  $options
534
     */
535 6
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
536
    {
537 6
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
538
        /** @var PersistentCollectionInterface[] $addToSetColls */
539 6
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
540
541 6
        $addToSetPayload = [];
542 6
        foreach ($addToSetColls as $propertyPath => $coll) {
543 6
            $callback                       = $this->getValuePrepareCallback($coll);
544 6
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
545 6
            $addToSetPayload[$propertyPath] = ['$each' => $value];
546
        }
547
548 6
        if (empty($addToSetPayload)) {
549
            return;
550
        }
551
552 6
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
553 6
    }
554
555
    /**
556
     * Return callback instance for specified collection. This callback will prepare values for query from documents
557
     * that collection contain.
558
     */
559 75
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
560
    {
561 75
        $mapping = $coll->getMapping();
562 75
        if (isset($mapping['embedded'])) {
563
            return function ($v) use ($mapping) {
564 39
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
565 39
            };
566
        }
567
568
        return function ($v) use ($mapping) {
569 37
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
570 37
        };
571
    }
572
573
    /**
574
     * Gets the parent information for a given PersistentCollection. It will
575
     * retrieve the top-level persistent Document that the PersistentCollection
576
     * lives in. We can use this to issue queries when updating a
577
     * PersistentCollection that is multiple levels deep inside an embedded
578
     * document.
579
     *
580
     *     <code>
581
     *     list($path, $parent) = $this->getPathAndParent($coll)
582
     *     </code>
583
     */
584 115
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
585
    {
586 115
        $mapping = $coll->getMapping();
587 115
        $fields  = [];
588 115
        $parent  = $coll->getOwner();
589 115
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
590 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...
591 18
            if (isset($m['reference'])) {
592
                break;
593
            }
594 18
            $parent   = $owner;
595 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...
596
        }
597 115
        $propertyPath = implode('.', array_reverse($fields));
598 115
        $path         = $mapping['name'];
599 115
        if ($propertyPath) {
600 18
            $path = $propertyPath . '.' . $path;
601
        }
602 115
        return [$path, $parent];
603
    }
604
605
    /**
606
     * Executes a query updating the given document.
607
     */
608 115
    private function executeQuery(object $document, array $newObj, array $options) : void
609
    {
610 115
        $className = get_class($document);
611 115
        $class     = $this->dm->getClassMetadata($className);
612 115
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
613 115
        $query     = ['_id' => $id];
614 115
        if ($class->isVersioned) {
615 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
616
        }
617 115
        $collection = $this->dm->getDocumentCollection($className);
618 115
        $result     = $collection->updateOne($query, $newObj, $options);
619 115
        if ($class->isVersioned && ! $result->getMatchedCount()) {
620 2
            throw LockException::lockFailed($document);
621
        }
622 113
    }
623
624
    /**
625
     * Remove from passed paths list all sub-paths.
626
     *
627
     * @param string[] $paths
628
     *
629
     * @return string[]
630
     */
631 110
    private function excludeSubPaths(array $paths) : array
632
    {
633 110
        if (empty($paths)) {
634
            return $paths;
635
        }
636 110
        sort($paths);
637 110
        $uniquePaths = [$paths[0]];
638 110
        for ($i = 1; $i < count($paths); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
639 6
            if (strpos($paths[$i], end($uniquePaths)) === 0) {
640 4
                continue;
641
            }
642
643 4
            $uniquePaths[] = $paths[$i];
644
        }
645
646 110
        return $uniquePaths;
647
    }
648
}
649