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