Completed
Push — master ( 95608f...d78a07 )
by Andreas
14s queued 10s
created

CollectionPersister::addToSetCollections()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3.0052

Importance

Changes 0
Metric Value
dl 0
loc 19
ccs 11
cts 12
cp 0.9167
rs 9.6333
c 0
b 0
f 0
cc 3
nc 4
nop 5
crap 3.0052
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
 * @internal
43
 */
44
final class CollectionPersister
45
{
46
    /** @var DocumentManager */
47
    private $dm;
48
49
    /** @var PersistenceBuilder */
50
    private $pb;
51
52
    /** @var UnitOfWork */
53
    private $uow;
54
55 1202
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
56
    {
57 1202
        $this->dm  = $dm;
58 1202
        $this->pb  = $pb;
59 1202
        $this->uow = $uow;
60 1202
    }
61
62
    /**
63
     * Deletes a PersistentCollection instances completely from a document using $unset.
64
     *
65
     * @param PersistentCollectionInterface[] $collections
66
     * @param array                           $options
67
     */
68 38
    public function delete(object $parent, array $collections, array $options) : void
69
    {
70 38
        $unsetPathsMap = [];
71
72 38
        foreach ($collections as $collection) {
73 38
            $mapping = $collection->getMapping();
74 38
            if ($mapping['isInverseSide']) {
75
                continue; // ignore inverse side
76
            }
77 38
            if (CollectionHelper::isAtomic($mapping['strategy'])) {
78
                throw new UnexpectedValueException($mapping['strategy'] . ' delete collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
79
            }
80 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...
81 38
            $unsetPathsMap[$propertyPath] = true;
82
        }
83
84 38
        if (empty($unsetPathsMap)) {
85
            return;
86
        }
87
88
        /** @var string[] $unsetPaths */
89 38
        $unsetPaths = array_keys($unsetPathsMap);
90
91 38
        $unsetPaths = array_fill_keys($this->excludeSubPaths($unsetPaths), true);
92 38
        $query      = ['$unset' => $unsetPaths];
93 38
        $this->executeQuery($parent, $query, $options);
94 37
    }
95
96
    /**
97
     * Updates a list PersistentCollection instances deleting removed rows and inserting new rows.
98
     *
99
     * @param PersistentCollectionInterface[] $collections
100
     * @param array                           $options
101
     */
102 106
    public function update(object $parent, array $collections, array $options) : void
103
    {
104 106
        $setStrategyColls     = [];
105 106
        $addPushStrategyColls = [];
106
107 106
        foreach ($collections as $coll) {
108 106
            $mapping = $coll->getMapping();
109
110 106
            if ($mapping['isInverseSide']) {
111
                continue; // ignore inverse side
112
            }
113 106
            switch ($mapping['strategy']) {
114
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET:
115
                case ClassMetadata::STORAGE_STRATEGY_ATOMIC_SET_ARRAY:
116
                    throw new UnexpectedValueException($mapping['strategy'] . ' update collection strategy should have been handled by DocumentPersister. Please report a bug in issue tracker');
117
118
                case ClassMetadata::STORAGE_STRATEGY_SET:
119
                case ClassMetadata::STORAGE_STRATEGY_SET_ARRAY:
120 14
                    $setStrategyColls[] = $coll;
121 14
                    break;
122
123
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
124
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
125 97
                    $addPushStrategyColls[] = $coll;
126 97
                    break;
127
128
                default:
129
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
130
            }
131
        }
132
133 106
        if (! empty($setStrategyColls)) {
134 14
            $this->setCollections($parent, $setStrategyColls, $options);
135
        }
136 106
        if (empty($addPushStrategyColls)) {
137 9
            return;
138
        }
139
140 97
        $this->deleteElements($parent, $addPushStrategyColls, $options);
141 97
        $this->insertElements($parent, $addPushStrategyColls, $options);
142 96
    }
143
144
    /**
145
     * Sets a list of PersistentCollection instances.
146
     *
147
     * This method is intended to be used with the "set" or "setArray"
148
     * strategies. The "setArray" strategy will ensure that the collections is
149
     * set as a BSON array, which means the collections elements will be
150
     * reindexed numerically before storage.
151
     *
152
     * @param PersistentCollectionInterface[] $collections
153
     * @param array                           $options
154
     */
155 14
    private function setCollections(object $parent, array $collections, array $options) : void
156
    {
157 14
        $pathCollMap = [];
158 14
        $paths       = [];
159 14
        foreach ($collections as $coll) {
160 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...
161 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...
162 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...
163
        }
164
165 14
        $paths = $this->excludeSubPaths($paths);
166
        /** @var PersistentCollectionInterface[] $setColls */
167 14
        $setColls   = array_intersect_key($pathCollMap, array_flip($paths));
168 14
        $setPayload = [];
169 14
        foreach ($setColls as $propertyPath => $coll) {
170 14
            $coll->initialize();
171 14
            $mapping                   = $coll->getMapping();
172 14
            $setData                   = $this->pb->prepareAssociatedCollectionValue(
173 14
                $coll,
174 14
                CollectionHelper::usesSet($mapping['strategy'])
175
            );
176 14
            $setPayload[$propertyPath] = $setData;
177
        }
178 14
        if (empty($setPayload)) {
179
            return;
180
        }
181
182 14
        $query = ['$set' => $setPayload];
183 14
        $this->executeQuery($parent, $query, $options);
184 14
    }
185
186
    /**
187
     * Deletes removed elements from a list of PersistentCollection instances.
188
     *
189
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
190
     *
191
     * @param PersistentCollectionInterface[] $collections
192
     * @param array                           $options
193
     */
194 97
    private function deleteElements(object $parent, array $collections, array $options) : void
195
    {
196 97
        $pathCollMap   = [];
197 97
        $paths         = [];
198 97
        $deleteDiffMap = [];
199
200 97
        foreach ($collections as $coll) {
201 97
            $coll->initialize();
202 97
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
203 2
                continue;
204
            }
205 95
            $deleteDiff = $coll->getDeleteDiff();
206
207 95
            if (empty($deleteDiff)) {
208 73
                continue;
209
            }
210 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...
211
212 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...
213 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...
214 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...
215
        }
216
217 97
        $paths        = $this->excludeSubPaths($paths);
218 97
        $deleteColls  = array_intersect_key($pathCollMap, array_flip($paths));
219 97
        $unsetPayload = [];
220 97
        $pullPayload  = [];
221 97
        foreach ($deleteColls as $propertyPath => $coll) {
222 31
            $deleteDiff = $deleteDiffMap[$propertyPath];
223 31
            foreach ($deleteDiff as $key => $document) {
224 31
                $unsetPayload[$propertyPath . '.' . $key] = true;
225
            }
226 31
            $pullPayload[$propertyPath] = null;
227
        }
228
229 97
        if (! empty($unsetPayload)) {
230 31
            $this->executeQuery($parent, ['$unset' => $unsetPayload], $options);
231
        }
232 97
        if (empty($pullPayload)) {
233 75
            return;
234
        }
235
236
        /**
237
         * @todo This is a hack right now because we don't have a proper way to
238
         * remove an element from an array by its key. Unsetting the key results
239
         * in the element being left in the array as null so we have to pull
240
         * null values.
241
         */
242 31
        $this->executeQuery($parent, ['$pull' => $pullPayload], $options);
243 31
    }
244
245
    /**
246
     * Inserts new elements for a PersistentCollection instances.
247
     *
248
     * This method is intended to be used with the "pushAll" and "addToSet" strategies.
249
     *
250
     * @param PersistentCollectionInterface[] $collections
251
     * @param array                           $options
252
     */
253 97
    private function insertElements(object $parent, array $collections, array $options) : void
254
    {
255 97
        $pushAllPathCollMap  = [];
256 97
        $addToSetPathCollMap = [];
257 97
        $pushAllPaths        = [];
258 97
        $addToSetPaths       = [];
259 97
        $diffsMap            = [];
260
261 97
        foreach ($collections as $coll) {
262 97
            $coll->initialize();
263 97
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
264 2
                continue;
265
            }
266 95
            $insertDiff = $coll->getInsertDiff();
267
268 95
            if (empty($insertDiff)) {
269 21
                continue;
270
            }
271
272 80
            $mapping  = $coll->getMapping();
273 80
            $strategy = $mapping['strategy'];
274
275 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...
276 80
            $diffsMap[$propertyPath] = $insertDiff;
277
278 80
            switch ($strategy) {
279
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
280 75
                    $pushAllPathCollMap[$propertyPath] = $coll;
281 75
                    $pushAllPaths[]                    = $propertyPath;
282 75
                    break;
283
284
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
285 7
                    $addToSetPathCollMap[$propertyPath] = $coll;
286 7
                    $addToSetPaths[]                    = $propertyPath;
287 7
                    break;
288
289
                default:
290
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
291
            }
292
        }
293
294 97
        if (! empty($pushAllPaths)) {
295 75
            $this->pushAllCollections(
296 75
                $parent,
297 75
                $pushAllPaths,
298 75
                $pushAllPathCollMap,
299 75
                $diffsMap,
300 75
                $options
301
            );
302
        }
303 96
        if (empty($addToSetPaths)) {
304 90
            return;
305
        }
306
307 7
        $this->addToSetCollections(
308 7
            $parent,
309 7
            $addToSetPaths,
310 7
            $addToSetPathCollMap,
311 7
            $diffsMap,
312 7
            $options
313
        );
314 7
    }
315
316
    /**
317
     * Perform collections update for 'pushAll' strategy.
318
     *
319
     * @param object $parent       Parent object to which passed collections is belong.
320
     * @param array  $collsPaths   Paths of collections that is passed.
321
     * @param array  $pathCollsMap List of collections indexed by their paths.
322
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
323
     * @param array  $options
324
     */
325 75
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
326
    {
327 75
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
328
        /** @var PersistentCollectionInterface[] $pushAllColls */
329 75
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
330 75
        $pushAllPayload = [];
331 75
        foreach ($pushAllColls as $propertyPath => $coll) {
332 75
            $callback                      = $this->getValuePrepareCallback($coll);
333 75
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
334 75
            $pushAllPayload[$propertyPath] = ['$each' => $value];
335
        }
336
337 75
        if (! empty($pushAllPayload)) {
338 75
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
339
        }
340
341 74
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
342 74
        foreach ($pushAllColls as $propertyPath => $coll) {
343 2
            $callback = $this->getValuePrepareCallback($coll);
344 2
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
345 2
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
346 2
            $this->executeQuery($parent, $query, $options);
347
        }
348 74
    }
349
350
    /**
351
     * Perform collections update by 'addToSet' strategy.
352
     *
353
     * @param object $parent       Parent object to which passed collections is belong.
354
     * @param array  $collsPaths   Paths of collections that is passed.
355
     * @param array  $pathCollsMap List of collections indexed by their paths.
356
     * @param array  $diffsMap     List of collection diffs indexed by collections paths.
357
     * @param array  $options
358
     */
359 7
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
360
    {
361 7
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
362
        /** @var PersistentCollectionInterface[] $addToSetColls */
363 7
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
364
365 7
        $addToSetPayload = [];
366 7
        foreach ($addToSetColls as $propertyPath => $coll) {
367 7
            $callback                       = $this->getValuePrepareCallback($coll);
368 7
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
369 7
            $addToSetPayload[$propertyPath] = ['$each' => $value];
370
        }
371
372 7
        if (empty($addToSetPayload)) {
373
            return;
374
        }
375
376 7
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
377 7
    }
378
379
    /**
380
     * Return callback instance for specified collection. This callback will prepare values for query from documents
381
     * that collection contain.
382
     */
383 80
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
384
    {
385 80
        $mapping = $coll->getMapping();
386 80
        if (isset($mapping['embedded'])) {
387
            return function ($v) use ($mapping) {
388 44
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
389 44
            };
390
        }
391
392
        return function ($v) use ($mapping) {
393 37
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
394 37
        };
395
    }
396
397
    /**
398
     * Gets the parent information for a given PersistentCollection. It will
399
     * retrieve the top-level persistent Document that the PersistentCollection
400
     * lives in. We can use this to issue queries when updating a
401
     * PersistentCollection that is multiple levels deep inside an embedded
402
     * document.
403
     *
404
     *     <code>
405
     *     list($path, $parent) = $this->getPathAndParent($coll)
406
     *     </code>
407
     */
408 118
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
409
    {
410 118
        $mapping = $coll->getMapping();
411 118
        $fields  = [];
412 118
        $parent  = $coll->getOwner();
413 118
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
414 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...
415 18
            if (isset($m['reference'])) {
416
                break;
417
            }
418 18
            $parent   = $owner;
419 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...
420
        }
421 118
        $propertyPath = implode('.', array_reverse($fields));
422 118
        $path         = $mapping['name'];
423 118
        if ($propertyPath) {
424 18
            $path = $propertyPath . '.' . $path;
425
        }
426
427 118
        return [$path, $parent];
428
    }
429
430
    /**
431
     * Executes a query updating the given document.
432
     */
433 118
    private function executeQuery(object $document, array $newObj, array $options) : void
434
    {
435 118
        $className = get_class($document);
436 118
        $class     = $this->dm->getClassMetadata($className);
437 118
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
438 118
        $query     = ['_id' => $id];
439 118
        if ($class->isVersioned) {
440 5
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
441
        }
442 118
        $collection = $this->dm->getDocumentCollection($className);
443 118
        $result     = $collection->updateOne($query, $newObj, $options);
444 118
        if ($class->isVersioned && ! $result->getMatchedCount()) {
445 2
            throw LockException::lockFailed($document);
446
        }
447 116
    }
448
449
    /**
450
     * Remove from passed paths list all sub-paths.
451
     *
452
     * @param string[] $paths
453
     *
454
     * @return string[]
455
     */
456 119
    private function excludeSubPaths(array $paths) : array
457
    {
458 119
        if (empty($paths)) {
459 75
            return $paths;
460
        }
461 118
        sort($paths);
462 118
        $uniquePaths = [$paths[0]];
463 118
        for ($i = 1, $count = count($paths); $i < $count; ++$i) {
464 12
            $lastUniquePath = end($uniquePaths);
465 12
            assert($lastUniquePath !== false);
466
467 12
            if (strpos($paths[$i], $lastUniquePath) === 0) {
468 10
                continue;
469
            }
470
471 4
            $uniquePaths[] = $paths[$i];
472
        }
473
474 118
        return $uniquePaths;
475
    }
476
}
477