Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

ODM/MongoDB/Persisters/CollectionPersister.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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);
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);
159 14
            $pathCollMap[$propertyPath] = $coll;
160 14
            $paths[]                    = $propertyPath;
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);
209
210 31
            $pathCollMap[$propertyPath]   = $coll;
211 31
            $paths[]                      = $propertyPath;
212 31
            $deleteDiffMap[$propertyPath] = $deleteDiff;
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);
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;
413 18
            if (isset($m['reference'])) {
414
                break;
415
            }
416 18
            $parent   = $owner;
417 18
            $fields[] = $field;
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) {
0 ignored issues
show
Accessing isVersioned on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
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