Completed
Push — master ( 55e1b9...dc9765 )
by Maciej
20s queued 10s
created

CollectionPersister::insertElements()   B

Complexity

Conditions 8
Paths 21

Size

Total Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 40
CRAP Score 8.0009

Importance

Changes 0
Metric Value
dl 0
loc 62
ccs 40
cts 41
cp 0.9756
rs 7.5846
c 0
b 0
f 0
cc 8
nc 21
nop 3
crap 8.0009

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 assert;
25
use function count;
26
use function end;
27
use function get_class;
28
use function implode;
29
use function sort;
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 1128
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
54
    {
55 1128
        $this->dm  = $dm;
56 1128
        $this->pb  = $pb;
57 1128
        $this->uow = $uow;
58 1128
    }
59
60
    /**
61
     * Deletes a PersistentCollection instances completely from a document using $unset.
62
     *
63
     * @param PersistentCollectionInterface[] $collections
64
     * @param array                           $options
65
     */
66 38
    public function delete(object $parent, array $collections, array $options) : void
67
    {
68 38
        $unsetPathsMap = [];
69
70 38
        foreach ($collections as $collection) {
71 38
            $mapping = $collection->getMapping();
72 38
            if ($mapping['isInverseSide']) {
73
                continue; // ignore inverse side
74
            }
75 38
            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 38
            [$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 38
            $unsetPathsMap[$propertyPath] = true;
80
        }
81
82 38
        if (empty($unsetPathsMap)) {
83
            return;
84
        }
85
86
        /** @var string[] $unsetPaths */
87 38
        $unsetPaths = array_keys($unsetPathsMap);
88
89 38
        $unsetPaths = array_fill_keys($this->excludeSubPaths($unsetPaths), true);
90 38
        $query      = ['$unset' => $unsetPaths];
91 38
        $this->executeQuery($parent, $query, $options);
92 37
    }
93
94
    /**
95
     * Updates a list PersistentCollection instances deleting removed rows and inserting new rows.
96
     *
97
     * @param PersistentCollectionInterface[] $collections
98
     * @param array                           $options
99
     */
100 106
    public function update(object $parent, array $collections, array $options) : void
101
    {
102 106
        $setStrategyColls     = [];
103 106
        $addPushStrategyColls = [];
104
105 106
        foreach ($collections as $coll) {
106 106
            $mapping = $coll->getMapping();
107
108 106
            if ($mapping['isInverseSide']) {
109
                continue; // ignore inverse side
110
            }
111 106
            switch ($mapping['strategy']) {
112
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
113
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
114
                    throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
115
116
                case ClassMetadata::STORAGE_STRATEGY_SET:
117
                case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
118 14
                    $setStrategyColls[] = $coll;
119 14
                    break;
120
121
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
122
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
123 97
                    $addPushStrategyColls[] = $coll;
124 97
                    break;
125
126
                default:
127
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
128
            }
129
        }
130
131 106
        if (! empty($setStrategyColls)) {
132 14
            $this->setCollections($parent, $setStrategyColls, $options);
133
        }
134 106
        if (empty($addPushStrategyColls)) {
135 9
            return;
136
        }
137
138 97
        $this->deleteElements($parent, $addPushStrategyColls, $options);
139 97
        $this->insertElements($parent, $addPushStrategyColls, $options);
140 96
    }
141
142
    /**
143
     * Sets a list of PersistentCollection instances.
144
     *
145
     * This method is intended to be used with the "set" or "setArray"
146
     * strategies. The "setArray" strategy will ensure that the collections is
147
     * set as a BSON array, which means the collections elements will be
148
     * reindexed numerically before storage.
149
     *
150
     * @param PersistentCollectionInterface[] $collections
151
     * @param array                           $options
152
     */
153 14
    private function setCollections(object $parent, array $collections, array $options) : void
154
    {
155 14
        $pathCollMap = [];
156 14
        $paths       = [];
157 14
        foreach ($collections as $coll) {
158 14
            [$propertyPath ]            = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

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

Let’s take a look at a simple example:

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

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

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

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

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

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

Let’s take a look at a simple example:

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

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

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

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

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

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

Let’s take a look at a simple example:

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

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

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

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

Loading history...
161
        }
162
163 14
        $paths = $this->excludeSubPaths($paths);
164
        /** @var PersistentCollectionInterface[] $setColls */
165 14
        $setColls   = array_intersect_key($pathCollMap, array_flip($paths));
166 14
        $setPayload = [];
167 14
        foreach ($setColls as $propertyPath => $coll) {
168 14
            $coll->initialize();
169 14
            $mapping                   = $coll->getMapping();
170 14
            $setData                   = $this->pb->prepareAssociatedCollectionValue(
171 14
                $coll,
172 14
                CollectionHelper::usesSet($mapping['strategy'])
173
            );
174 14
            $setPayload[$propertyPath] = $setData;
175
        }
176 14
        if (empty($setPayload)) {
177
            return;
178
        }
179
180 14
        $query = ['$set' => $setPayload];
181 14
        $this->executeQuery($parent, $query, $options);
182 14
    }
183
184
    /**
185
     * Deletes removed elements from a list of PersistentCollection instances.
186
     *
187
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
188
     *
189
     * @param PersistentCollectionInterface[] $collections
190
     * @param array                           $options
191
     */
192 97
    private function deleteElements(object $parent, array $collections, array $options) : void
193
    {
194 97
        $pathCollMap   = [];
195 97
        $paths         = [];
196 97
        $deleteDiffMap = [];
197
198 97
        foreach ($collections as $coll) {
199 97
            $coll->initialize();
200 97
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
201 2
                continue;
202
            }
203 95
            $deleteDiff = $coll->getDeleteDiff();
204
205 95
            if (empty($deleteDiff)) {
206 73
                continue;
207
            }
208 31
            [$propertyPath ] = $this->getPathAndParent($coll);
0 ignored issues
show
Bug introduced by
The variable $propertyPath seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

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

Let’s take a look at a simple example:

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

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

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

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

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

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

Let’s take a look at a simple example:

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

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

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

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

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

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

Let’s take a look at a simple example:

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

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

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

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

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

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

Let’s take a look at a simple example:

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

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

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

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

Loading history...
213
        }
214
215 97
        $paths        = $this->excludeSubPaths($paths);
216 97
        $deleteColls  = array_intersect_key($pathCollMap, array_flip($paths));
217 97
        $unsetPayload = [];
218 97
        $pullPayload  = [];
219 97
        foreach ($deleteColls as $propertyPath => $coll) {
220 31
            $deleteDiff = $deleteDiffMap[$propertyPath];
221 31
            foreach ($deleteDiff as $key => $document) {
222 31
                $unsetPayload[$propertyPath . '.' . $key] = true;
223
            }
224 31
            $pullPayload[$propertyPath] = null;
225
        }
226
227 97
        if (! empty($unsetPayload)) {
228 31
            $this->executeQuery($parent, ['$unset' => $unsetPayload], $options);
229
        }
230 97
        if (empty($pullPayload)) {
231 75
            return;
232
        }
233
234
        /**
235
         * @todo This is a hack right now because we don't have a proper way to
236
         * remove an element from an array by its key. Unsetting the key results
237
         * in the element being left in the array as null so we have to pull
238
         * null values.
239
         */
240 31
        $this->executeQuery($parent, ['$pull' => $pullPayload], $options);
241 31
    }
242
243
    /**
244
     * Inserts new elements for a PersistentCollection instances.
245
     *
246
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
247
     *
248
     * @param PersistentCollectionInterface[] $collections
249
     * @param array                           $options
250
     */
251 97
    private function insertElements(object $parent, array $collections, array $options) : void
252
    {
253 97
        $pushAllPathCollMap  = [];
254 97
        $addToSetPathCollMap = [];
255 97
        $pushAllPaths        = [];
256 97
        $addToSetPaths       = [];
257 97
        $diffsMap            = [];
258
259 97
        foreach ($collections as $coll) {
260 97
            $coll->initialize();
261 97
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
262 2
                continue;
263
            }
264 95
            $insertDiff = $coll->getInsertDiff();
265
266 95
            if (empty($insertDiff)) {
267 21
                continue;
268
            }
269
270 80
            $mapping  = $coll->getMapping();
271 80
            $strategy = $mapping['strategy'];
272
273 80
            [$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...
274 80
            $diffsMap[$propertyPath] = $insertDiff;
275
276 80
            switch ($strategy) {
277
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
278 75
                    $pushAllPathCollMap[$propertyPath] = $coll;
279 75
                    $pushAllPaths[]                    = $propertyPath;
280 75
                    break;
281
282
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
283 7
                    $addToSetPathCollMap[$propertyPath] = $coll;
284 7
                    $addToSetPaths[]                    = $propertyPath;
285 7
                    break;
286
287
                default:
288
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
289
            }
290
        }
291
292 97
        if (! empty($pushAllPaths)) {
293 75
            $this->pushAllCollections(
294 75
                $parent,
295 75
                $pushAllPaths,
296 75
                $pushAllPathCollMap,
297 75
                $diffsMap,
298 75
                $options
299
            );
300
        }
301 96
        if (empty($addToSetPaths)) {
302 90
            return;
303
        }
304
305 7
        $this->addToSetCollections(
306 7
            $parent,
307 7
            $addToSetPaths,
308 7
            $addToSetPathCollMap,
309 7
            $diffsMap,
310 7
            $options
311
        );
312 7
    }
313
314
    /**
315
     * Perform collections update for 'pushAll' strategy.
316
     *
317
     * @param object $parent       Parent object to which passed collections is belong.
318
     * @param array  $collsPaths   Paths of collections that is passed.
319
     * @param array  $pathCollsMap List of collections indexed by their paths.
320
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
321
     * @param array  $options
322
     */
323 75
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
324
    {
325 75
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
326
        /** @var PersistentCollectionInterface[] $pushAllColls */
327 75
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
328 75
        $pushAllPayload = [];
329 75
        foreach ($pushAllColls as $propertyPath => $coll) {
330 75
            $callback                      = $this->getValuePrepareCallback($coll);
331 75
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
332 75
            $pushAllPayload[$propertyPath] = ['$each' => $value];
333
        }
334
335 75
        if (! empty($pushAllPayload)) {
336 75
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
337
        }
338
339 74
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
340 74
        foreach ($pushAllColls as $propertyPath => $coll) {
341 2
            $callback = $this->getValuePrepareCallback($coll);
342 2
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
343 2
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
344 2
            $this->executeQuery($parent, $query, $options);
345
        }
346 74
    }
347
348
    /**
349
     * Perform collections update by 'addToSet' strategy.
350
     *
351
     * @param object $parent       Parent object to which passed collections is belong.
352
     * @param array  $collsPaths   Paths of collections that is passed.
353
     * @param array  $pathCollsMap List of collections indexed by their paths.
354
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
355
     * @param array  $options
356
     */
357 7
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
358
    {
359 7
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
360
        /** @var PersistentCollectionInterface[] $addToSetColls */
361 7
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
362
363 7
        $addToSetPayload = [];
364 7
        foreach ($addToSetColls as $propertyPath => $coll) {
365 7
            $callback                       = $this->getValuePrepareCallback($coll);
366 7
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
367 7
            $addToSetPayload[$propertyPath] = ['$each' => $value];
368
        }
369
370 7
        if (empty($addToSetPayload)) {
371
            return;
372
        }
373
374 7
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
375 7
    }
376
377
    /**
378
     * Return callback instance for specified collection. This callback will prepare values for query from documents
379
     * that collection contain.
380
     */
381 80
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
382
    {
383 80
        $mapping = $coll->getMapping();
384 80
        if (isset($mapping['embedded'])) {
385
            return function ($v) use ($mapping) {
386 44
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
387 44
            };
388
        }
389
390
        return function ($v) use ($mapping) {
391 37
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
392 37
        };
393
    }
394
395
    /**
396
     * Gets the parent information for a given PersistentCollection. It will
397
     * retrieve the top-level persistent Document that the PersistentCollection
398
     * lives in. We can use this to issue queries when updating a
399
     * PersistentCollection that is multiple levels deep inside an embedded
400
     * document.
401
     *
402
     *     <code>
403
     *     list($path, $parent) = $this->getPathAndParent($coll)
404
     *     </code>
405
     */
406 118
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
407
    {
408 118
        $mapping = $coll->getMapping();
409 118
        $fields  = [];
410 118
        $parent  = $coll->getOwner();
411 118
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
412 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...
413 18
            if (isset($m['reference'])) {
414
                break;
415
            }
416 18
            $parent   = $owner;
417 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...
418
        }
419 118
        $propertyPath = implode('.', array_reverse($fields));
420 118
        $path         = $mapping['name'];
421 118
        if ($propertyPath) {
422 18
            $path = $propertyPath . '.' . $path;
423
        }
424 118
        return [$path, $parent];
425
    }
426
427
    /**
428
     * Executes a query updating the given document.
429
     */
430 118
    private function executeQuery(object $document, array $newObj, array $options) : void
431
    {
432 118
        $className = get_class($document);
433 118
        $class     = $this->dm->getClassMetadata($className);
434 118
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
435 118
        $query     = ['_id' => $id];
436 118
        if ($class->isVersioned) {
437 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
438
        }
439 118
        $collection = $this->dm->getDocumentCollection($className);
440 118
        $result     = $collection->updateOne($query, $newObj, $options);
441 118
        if ($class->isVersioned && ! $result->getMatchedCount()) {
442 2
            throw LockException::lockFailed($document);
443
        }
444 116
    }
445
446
    /**
447
     * Remove from passed paths list all sub-paths.
448
     *
449
     * @param string[] $paths
450
     *
451
     * @return string[]
452
     */
453 119
    private function excludeSubPaths(array $paths) : array
454
    {
455 119
        if (empty($paths)) {
456 75
            return $paths;
457
        }
458 118
        sort($paths);
459 118
        $uniquePaths = [$paths[0]];
460 118
        for ($i = 1, $count = count($paths); $i < $count; ++$i) {
461 12
            $lastUniquePath = end($uniquePaths);
462 12
            assert($lastUniquePath !== false);
463
464 12
            if (strpos($paths[$i], $lastUniquePath) === 0) {
465 10
                continue;
466
            }
467
468 4
            $uniquePaths[] = $paths[$i];
469
        }
470
471 118
        return $uniquePaths;
472
    }
473
}
474