Completed
Push — master ( b90edc...95608f )
by Andreas
14:15 queued 11s
created

CollectionPersister::executeQuery()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 0
cts 12
cp 0
rs 9.7666
c 0
b 0
f 0
cc 4
nc 4
nop 3
crap 20
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 562
    public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfWork $uow)
56
    {
57 562
        $this->dm  = $dm;
58 562
        $this->pb  = $pb;
59 562
        $this->uow = $uow;
60 562
    }
61
62
    /**
63
     * Deletes a PersistentCollection instances completely from a document using $unset.
64
     *
65
     * @param PersistentCollectionInterface[] $collections
66
     * @param array                           $options
67
     */
68
    public function delete(object $parent, array $collections, array $options) : void
69
    {
70
        $unsetPathsMap = [];
71
72
        foreach ($collections as $collection) {
73
            $mapping = $collection->getMapping();
74
            if ($mapping['isInverseSide']) {
75
                continue; // ignore inverse side
76
            }
77
            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
            [$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
            $unsetPathsMap[$propertyPath] = true;
82
        }
83
84
        if (empty($unsetPathsMap)) {
85
            return;
86
        }
87
88
        /** @var string[] $unsetPaths */
89
        $unsetPaths = array_keys($unsetPathsMap);
90
91
        $unsetPaths = array_fill_keys($this->excludeSubPaths($unsetPaths), true);
92
        $query      = ['$unset' => $unsetPaths];
93
        $this->executeQuery($parent, $query, $options);
94
    }
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
    public function update(object $parent, array $collections, array $options) : void
103
    {
104
        $setStrategyColls     = [];
105
        $addPushStrategyColls = [];
106
107
        foreach ($collections as $coll) {
108
            $mapping = $coll->getMapping();
109
110
            if ($mapping['isInverseSide']) {
111
                continue; // ignore inverse side
112
            }
113
            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
                    $setStrategyColls[] = $coll;
121
                    break;
122
123
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
124
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
125
                    $addPushStrategyColls[] = $coll;
126
                    break;
127
128
                default:
129
                    throw new UnexpectedValueException('Unsupported collection strategy: ' . $mapping['strategy']);
130
            }
131
        }
132
133
        if (! empty($setStrategyColls)) {
134
            $this->setCollections($parent, $setStrategyColls, $options);
135
        }
136
        if (empty($addPushStrategyColls)) {
137
            return;
138
        }
139
140
        $this->deleteElements($parent, $addPushStrategyColls, $options);
141
        $this->insertElements($parent, $addPushStrategyColls, $options);
142
    }
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
    private function setCollections(object $parent, array $collections, array $options) : void
156
    {
157
        $pathCollMap = [];
158
        $paths       = [];
159
        foreach ($collections as $coll) {
160
            [$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
            $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
            $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
        $paths = $this->excludeSubPaths($paths);
166
        /** @var PersistentCollectionInterface[] $setColls */
167
        $setColls   = array_intersect_key($pathCollMap, array_flip($paths));
168
        $setPayload = [];
169
        foreach ($setColls as $propertyPath => $coll) {
170
            $coll->initialize();
171
            $mapping                   = $coll->getMapping();
172
            $setData                   = $this->pb->prepareAssociatedCollectionValue(
173
                $coll,
174
                CollectionHelper::usesSet($mapping['strategy'])
175
            );
176
            $setPayload[$propertyPath] = $setData;
177
        }
178
        if (empty($setPayload)) {
179
            return;
180
        }
181
182
        $query = ['$set' => $setPayload];
183
        $this->executeQuery($parent, $query, $options);
184
    }
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
    private function deleteElements(object $parent, array $collections, array $options) : void
195
    {
196
        $pathCollMap   = [];
197
        $paths         = [];
198
        $deleteDiffMap = [];
199
200
        foreach ($collections as $coll) {
201
            $coll->initialize();
202
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
203
                continue;
204
            }
205
            $deleteDiff = $coll->getDeleteDiff();
206
207
            if (empty($deleteDiff)) {
208
                continue;
209
            }
210
            [$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
            $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
            $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
            $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
        $paths        = $this->excludeSubPaths($paths);
218
        $deleteColls  = array_intersect_key($pathCollMap, array_flip($paths));
219
        $unsetPayload = [];
220
        $pullPayload  = [];
221
        foreach ($deleteColls as $propertyPath => $coll) {
222
            $deleteDiff = $deleteDiffMap[$propertyPath];
223
            foreach ($deleteDiff as $key => $document) {
224
                $unsetPayload[$propertyPath . '.' . $key] = true;
225
            }
226
            $pullPayload[$propertyPath] = null;
227
        }
228
229
        if (! empty($unsetPayload)) {
230
            $this->executeQuery($parent, ['$unset' => $unsetPayload], $options);
231
        }
232
        if (empty($pullPayload)) {
233
            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
        $this->executeQuery($parent, ['$pull' => $pullPayload], $options);
243
    }
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
    private function insertElements(object $parent, array $collections, array $options) : void
254
    {
255
        $pushAllPathCollMap  = [];
256
        $addToSetPathCollMap = [];
257
        $pushAllPaths        = [];
258
        $addToSetPaths       = [];
259
        $diffsMap            = [];
260
261
        foreach ($collections as $coll) {
262
            $coll->initialize();
263
            if (! $this->uow->isCollectionScheduledForUpdate($coll)) {
264
                continue;
265
            }
266
            $insertDiff = $coll->getInsertDiff();
267
268
            if (empty($insertDiff)) {
269
                continue;
270
            }
271
272
            $mapping  = $coll->getMapping();
273
            $strategy = $mapping['strategy'];
274
275
            [$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
            $diffsMap[$propertyPath] = $insertDiff;
277
278
            switch ($strategy) {
279
                case ClassMetadata::STORAGE_STRATEGY_PUSH_ALL:
280
                    $pushAllPathCollMap[$propertyPath] = $coll;
281
                    $pushAllPaths[]                    = $propertyPath;
282
                    break;
283
284
                case ClassMetadata::STORAGE_STRATEGY_ADD_TO_SET:
285
                    $addToSetPathCollMap[$propertyPath] = $coll;
286
                    $addToSetPaths[]                    = $propertyPath;
287
                    break;
288
289
                default:
290
                    throw new LogicException('Invalid strategy ' . $strategy . ' given for insertCollections');
291
            }
292
        }
293
294
        if (! empty($pushAllPaths)) {
295
            $this->pushAllCollections(
296
                $parent,
297
                $pushAllPaths,
298
                $pushAllPathCollMap,
299
                $diffsMap,
300
                $options
301
            );
302
        }
303
        if (empty($addToSetPaths)) {
304
            return;
305
        }
306
307
        $this->addToSetCollections(
308
            $parent,
309
            $addToSetPaths,
310
            $addToSetPathCollMap,
311
            $diffsMap,
312
            $options
313
        );
314
    }
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
    private function pushAllCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
326
    {
327
        $pushAllPaths = $this->excludeSubPaths($collsPaths);
328
        /** @var PersistentCollectionInterface[] $pushAllColls */
329
        $pushAllColls   = array_intersect_key($pathCollsMap, array_flip($pushAllPaths));
330
        $pushAllPayload = [];
331
        foreach ($pushAllColls as $propertyPath => $coll) {
332
            $callback                      = $this->getValuePrepareCallback($coll);
333
            $value                         = array_values(array_map($callback, $diffsMap[$propertyPath]));
334
            $pushAllPayload[$propertyPath] = ['$each' => $value];
335
        }
336
337
        if (! empty($pushAllPayload)) {
338
            $this->executeQuery($parent, ['$push' => $pushAllPayload], $options);
339
        }
340
341
        $pushAllColls = array_diff_key($pathCollsMap, array_flip($pushAllPaths));
342
        foreach ($pushAllColls as $propertyPath => $coll) {
343
            $callback = $this->getValuePrepareCallback($coll);
344
            $value    = array_values(array_map($callback, $diffsMap[$propertyPath]));
345
            $query    = ['$push' => [$propertyPath => ['$each' => $value]]];
346
            $this->executeQuery($parent, $query, $options);
347
        }
348
    }
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
    private function addToSetCollections(object $parent, array $collsPaths, array $pathCollsMap, array $diffsMap, array $options) : void
360
    {
361
        $addToSetPaths = $this->excludeSubPaths($collsPaths);
362
        /** @var PersistentCollectionInterface[] $addToSetColls */
363
        $addToSetColls = array_intersect_key($pathCollsMap, array_flip($addToSetPaths));
364
365
        $addToSetPayload = [];
366
        foreach ($addToSetColls as $propertyPath => $coll) {
367
            $callback                       = $this->getValuePrepareCallback($coll);
368
            $value                          = array_values(array_map($callback, $diffsMap[$propertyPath]));
369
            $addToSetPayload[$propertyPath] = ['$each' => $value];
370
        }
371
372
        if (empty($addToSetPayload)) {
373
            return;
374
        }
375
376
        $this->executeQuery($parent, ['$addToSet' => $addToSetPayload], $options);
377
    }
378
379
    /**
380
     * Return callback instance for specified collection. This callback will prepare values for query from documents
381
     * that collection contain.
382
     */
383
    private function getValuePrepareCallback(PersistentCollectionInterface $coll) : Closure
384
    {
385
        $mapping = $coll->getMapping();
386
        if (isset($mapping['embedded'])) {
387
            return function ($v) use ($mapping) {
388
                return $this->pb->prepareEmbeddedDocumentValue($mapping, $v);
389
            };
390
        }
391
392
        return function ($v) use ($mapping) {
393
            return $this->pb->prepareReferencedDocumentValue($mapping, $v);
394
        };
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
    private function getPathAndParent(PersistentCollectionInterface $coll) : array
409
    {
410
        $mapping = $coll->getMapping();
411
        $fields  = [];
412
        $parent  = $coll->getOwner();
413
        while (($association = $this->uow->getParentAssociation($parent)) !== null) {
414
            [$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
            if (isset($m['reference'])) {
416
                break;
417
            }
418
            $parent   = $owner;
419
            $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
        $propertyPath = implode('.', array_reverse($fields));
422
        $path         = $mapping['name'];
423
        if ($propertyPath) {
424
            $path = $propertyPath . '.' . $path;
425
        }
426
427
        return [$path, $parent];
428
    }
429
430
    /**
431
     * Executes a query updating the given document.
432
     */
433
    private function executeQuery(object $document, array $newObj, array $options) : void
434
    {
435
        $className = get_class($document);
436
        $class     = $this->dm->getClassMetadata($className);
437
        $id        = $class->getDatabaseIdentifierValue($this->uow->getDocumentIdentifier($document));
438
        $query     = ['_id' => $id];
439
        if ($class->isVersioned) {
440
            $query[$class->fieldMappings[$class->versionField]['name']] = $class->reflFields[$class->versionField]->getValue($document);
441
        }
442
        $collection = $this->dm->getDocumentCollection($className);
443
        $result     = $collection->updateOne($query, $newObj, $options);
444
        if ($class->isVersioned && ! $result->getMatchedCount()) {
445
            throw LockException::lockFailed($document);
446
        }
447
    }
448
449
    /**
450
     * Remove from passed paths list all sub-paths.
451
     *
452
     * @param string[] $paths
453
     *
454
     * @return string[]
455
     */
456
    private function excludeSubPaths(array $paths) : array
457
    {
458
        if (empty($paths)) {
459
            return $paths;
460
        }
461
        sort($paths);
462
        $uniquePaths = [$paths[0]];
463
        for ($i = 1, $count = count($paths); $i < $count; ++$i) {
464
            $lastUniquePath = end($uniquePaths);
465
            assert($lastUniquePath !== false);
466
467
            if (strpos($paths[$i], $lastUniquePath) === 0) {
468
                continue;
469
            }
470
471
            $uniquePaths[] = $paths[$i];
472
        }
473
474
        return $uniquePaths;
475
    }
476
}
477