Completed
Push — master ( 183805...3c0652 )
by Andreas
17:46
created

SchemaManager::isEquivalentIndexKeys()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 8
cts 8
cp 1
rs 9.536
c 0
b 0
f 0
cc 3
nc 2
nop 2
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
8
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory;
9
use MongoDB\Driver\Exception\RuntimeException;
10
use MongoDB\Model\IndexInfo;
11
use function array_filter;
12
use function array_unique;
13
use function iterator_to_array;
14
use function ksort;
15
use function strpos;
16
17
class SchemaManager
18
{
19
    /** @var DocumentManager */
20
    protected $dm;
21
22
    /** @var ClassMetadataFactory */
23
    protected $metadataFactory;
24
25 1644
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
26
    {
27 1644
        $this->dm = $dm;
28 1644
        $this->metadataFactory = $cmf;
29 1644
    }
30
31
    /**
32
     * Ensure indexes are created for all documents that can be loaded with the
33
     * metadata factory.
34
     *
35
     * @param int $timeout Timeout (ms) for acknowledged index creation
36
     */
37 1 View Code Duplication
    public function ensureIndexes($timeout = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
38
    {
39 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
40 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
41 1
                continue;
42
            }
43 1
            $this->ensureDocumentIndexes($class->name, $timeout);
44
        }
45 1
    }
46
47
    /**
48
     * Ensure indexes exist for all mapped document classes.
49
     *
50
     * Indexes that exist in MongoDB but not the document metadata will be
51
     * deleted.
52
     *
53
     * @param int $timeout Timeout (ms) for acknowledged index creation
54
     */
55 View Code Duplication
    public function updateIndexes($timeout = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
56
    {
57
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
58
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
59
                continue;
60
            }
61
            $this->updateDocumentIndexes($class->name, $timeout);
62
        }
63
    }
64
65
    /**
66
     * Ensure indexes exist for the mapped document class.
67
     *
68
     * Indexes that exist in MongoDB but not the document metadata will be
69
     * deleted.
70
     *
71
     * @param string $documentName
72
     * @param int    $timeout      Timeout (ms) for acknowledged index creation
73
     * @throws \InvalidArgumentException
74
     */
75 3
    public function updateDocumentIndexes($documentName, $timeout = null)
76
    {
77 3
        $class = $this->dm->getClassMetadata($documentName);
78
79 3
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
80
            throw new \InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.');
81
        }
82
83 3
        $documentIndexes = $this->getDocumentIndexes($documentName);
84 3
        $collection = $this->dm->getDocumentCollection($documentName);
85 3
        $mongoIndexes = iterator_to_array($collection->listIndexes());
86
87
        /* Determine which Mongo indexes should be deleted. Exclude the ID index
88
         * and those that are equivalent to any in the class metadata.
89
         */
90 3
        $self = $this;
91
        $mongoIndexes = array_filter($mongoIndexes, function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
92 1
            if ($mongoIndex['name'] === '_id_') {
93
                return false;
94
            }
95
96 1
            foreach ($documentIndexes as $documentIndex) {
97 1
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
98 1
                    return false;
99
                }
100
            }
101
102 1
            return true;
103 3
        });
104
105
        // Delete indexes that do not exist in class metadata
106 3
        foreach ($mongoIndexes as $mongoIndex) {
107 1
            if (! isset($mongoIndex['name'])) {
108
                continue;
109
            }
110
111 1
            $collection->dropIndex($mongoIndex['name']);
112
        }
113
114 3
        $this->ensureDocumentIndexes($documentName, $timeout);
115 3
    }
116
117
    /**
118
     * @param string $documentName
119
     * @return array
120
     */
121 19
    public function getDocumentIndexes($documentName)
122
    {
123 19
        $visited = [];
124 19
        return $this->doGetDocumentIndexes($documentName, $visited);
125
    }
126
127
    /**
128
     * @param string $documentName
129
     * @param array  $visited
130
     * @return array
131
     */
132 19
    private function doGetDocumentIndexes($documentName, array &$visited)
133
    {
134 19
        if (isset($visited[$documentName])) {
135 1
            return [];
136
        }
137
138 19
        $visited[$documentName] = true;
139
140 19
        $class = $this->dm->getClassMetadata($documentName);
141 19
        $indexes = $this->prepareIndexes($class);
142 19
        $embeddedDocumentIndexes = [];
143
144
        // Add indexes from embedded & referenced documents
145 19
        foreach ($class->fieldMappings as $fieldMapping) {
146 19
            if (isset($fieldMapping['embedded'])) {
147 3
                if (isset($fieldMapping['targetDocument'])) {
148 3
                    $possibleEmbeds = [$fieldMapping['targetDocument']];
149 2
                } elseif (isset($fieldMapping['discriminatorMap'])) {
150 1
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
151
                } else {
152 1
                    continue;
153
                }
154 3
                foreach ($possibleEmbeds as $embed) {
155 3
                    if (isset($embeddedDocumentIndexes[$embed])) {
156 2
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
157
                    } else {
158 3
                        $embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited);
159 3
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
160
                    }
161 3
                    foreach ($embeddedIndexes as $embeddedIndex) {
162 2
                        foreach ($embeddedIndex['keys'] as $key => $value) {
163 2
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
164 2
                            unset($embeddedIndex['keys'][$key]);
165
                        }
166 3
                        $indexes[] = $embeddedIndex;
167
                    }
168
                }
169 19
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
170 8
                foreach ($indexes as $idx => $index) {
171 8
                    $newKeys = [];
172 8
                    foreach ($index['keys'] as $key => $v) {
173 8
                        if ($key === $fieldMapping['name']) {
174 2
                            $key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key);
175
                        }
176 8
                        $newKeys[$key] = $v;
177
                    }
178 19
                    $indexes[$idx]['keys'] = $newKeys;
179
                }
180
            }
181
        }
182 19
        return $indexes;
183
    }
184
185
    /**
186
     * @return array
187
     */
188 19
    private function prepareIndexes(ClassMetadata $class)
189
    {
190 19
        $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
191 19
        $indexes = $class->getIndexes();
192 19
        $newIndexes = [];
193
194 19
        foreach ($indexes as $index) {
195
            $newIndex = [
196 19
                'keys' => [],
197 19
                'options' => $index['options'],
198
            ];
199 19
            foreach ($index['keys'] as $key => $value) {
200 19
                $key = $persister->prepareFieldName($key);
201 19
                if ($class->hasField($key)) {
202 17
                    $mapping = $class->getFieldMapping($key);
203 17
                    $newIndex['keys'][$mapping['name']] = $value;
204
                } else {
205 19
                    $newIndex['keys'][$key] = $value;
206
                }
207
            }
208
209 19
            $newIndexes[] = $newIndex;
210
        }
211
212 19
        return $newIndexes;
213
    }
214
215
    /**
216
     * Ensure the given document's indexes are created.
217
     *
218
     * @param string $documentName
219
     * @param int    $timeout      Timeout (ms) for acknowledged index creation
220
     * @throws \InvalidArgumentException
221
     */
222 15
    public function ensureDocumentIndexes($documentName, $timeout = null)
223
    {
224 15
        $class = $this->dm->getClassMetadata($documentName);
225 15
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
226
            throw new \InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
227
        }
228
229 15
        $indexes = $this->getDocumentIndexes($documentName);
230 15
        if (! $indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
231 3
            return;
232
        }
233
234 15
        $collection = $this->dm->getDocumentCollection($class->name);
235 15
        foreach ($indexes as $index) {
236 15
            $keys = $index['keys'];
237 15
            $options = $index['options'];
238
239 15
            if (! isset($options['timeout']) && isset($timeout)) {
240 1
                $options['timeout'] = $timeout;
241
            }
242
243 15
            $collection->createIndex($keys, $options);
244
        }
245 15
    }
246
247
    /**
248
     * Delete indexes for all documents that can be loaded with the
249
     * metadata factory.
250
     */
251 1 View Code Duplication
    public function deleteIndexes()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
252
    {
253 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
254 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
255 1
                continue;
256
            }
257 1
            $this->deleteDocumentIndexes($class->name);
258
        }
259 1
    }
260
261
    /**
262
     * Delete the given document's indexes.
263
     *
264
     * @param string $documentName
265
     * @throws \InvalidArgumentException
266
     */
267 2 View Code Duplication
    public function deleteDocumentIndexes($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268
    {
269 2
        $class = $this->dm->getClassMetadata($documentName);
270 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
271
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
272
        }
273 2
        $this->dm->getDocumentCollection($documentName)->dropIndexes();
274 2
    }
275
276
    /**
277
     * Create all the mapped document collections in the metadata factory.
278
     */
279 1 View Code Duplication
    public function createCollections()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
    {
281 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
282 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
283 1
                continue;
284
            }
285 1
            $this->createDocumentCollection($class->name);
286
        }
287 1
    }
288
289
    /**
290
     * Create the document collection for a mapped class.
291
     *
292
     * @param string $documentName
293
     * @throws \InvalidArgumentException
294
     */
295 4
    public function createDocumentCollection($documentName)
296
    {
297 4
        $class = $this->dm->getClassMetadata($documentName);
298
299 4
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
300
            throw new \InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
301
        }
302
303 4
        $this->dm->getDocumentDatabase($documentName)->createCollection(
304 4
            $class->getCollection(),
305
            [
306 4
                'capped' => $class->getCollectionCapped(),
307 4
                'size' => $class->getCollectionSize(),
308 4
                'max' => $class->getCollectionMax(),
309
            ]
310
        );
311 4
    }
312
313
    /**
314
     * Drop all the mapped document collections in the metadata factory.
315
     */
316 1 View Code Duplication
    public function dropCollections()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
    {
318 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
319 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
320 1
                continue;
321
            }
322 1
            $this->dropDocumentCollection($class->name);
323
        }
324 1
    }
325
326
    /**
327
     * Drop the document collection for a mapped class.
328
     *
329
     * @param string $documentName
330
     * @throws \InvalidArgumentException
331
     */
332 4 View Code Duplication
    public function dropDocumentCollection($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
333
    {
334 4
        $class = $this->dm->getClassMetadata($documentName);
335 4
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
336
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
337
        }
338 4
        $this->dm->getDocumentCollection($documentName)->drop();
339 4
    }
340
341
    /**
342
     * Drop all the mapped document databases in the metadata factory.
343
     */
344 1 View Code Duplication
    public function dropDatabases()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
345
    {
346 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
347 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
348 1
                continue;
349
            }
350 1
            $this->dropDocumentDatabase($class->name);
351
        }
352 1
    }
353
354
    /**
355
     * Drop the document database for a mapped class.
356
     *
357
     * @param string $documentName
358
     * @throws \InvalidArgumentException
359
     */
360 2 View Code Duplication
    public function dropDocumentDatabase($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361
    {
362 2
        $class = $this->dm->getClassMetadata($documentName);
363 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
364
            throw new \InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
365
        }
366 2
        $this->dm->getDocumentDatabase($documentName)->drop();
367 2
    }
368
369
    /**
370
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
371
     * considered equivalent to an index in class metadata.
372
     *
373
     * Indexes are considered different if:
374
     *
375
     *   (a) Key/direction pairs differ or are not in the same order
376
     *   (b) Sparse or unique options differ
377
     *   (c) Mongo index is unique without dropDups and mapped index is unique
378
     *       with dropDups
379
     *   (d) Geospatial options differ (bits, max, min)
380
     *   (e) The partialFilterExpression differs
381
     *
382
     * Regarding (c), the inverse case is not a reason to delete and
383
     * recreate the index, since dropDups only affects creation of
384
     * the unique index. Additionally, the background option is only
385
     * relevant to index creation and is not considered.
386
     *
387
     * @param array|IndexInfo $mongoIndex    Mongo index data.
388
     * @param array           $documentIndex Document index data.
389
     * @return bool True if the indexes are equivalent, otherwise false.
390
     */
391 47
    public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)
392
    {
393 47
        $documentIndexOptions = $documentIndex['options'];
394
395 47
        if (! $this->isEquivalentIndexKeys($mongoIndex, $documentIndex)) {
396 4
            return false;
397
        }
398
399 43
        if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) {
400 2
            return false;
401
        }
402
403 41
        if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) {
404 2
            return false;
405
        }
406
407 39
        if (! empty($mongoIndex['unique']) && empty($mongoIndex['dropDups']) &&
408 39
            ! empty($documentIndexOptions['unique']) && ! empty($documentIndexOptions['dropDups'])) {
409 1
            return false;
410
        }
411
412 38
        foreach (['bits', 'max', 'min'] as $option) {
413 38
            if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) {
414 6
                return false;
415
            }
416
417 36 View Code Duplication
            if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
418 36
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
419 36
                return false;
420
            }
421
        }
422
423 29
        if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
424 2
            return false;
425
        }
426
427 27
        if (isset($mongoIndex['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
428 27
            $mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
429 1
            return false;
430
        }
431
432 26
        if (isset($mongoIndex['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
433 2
            return false;
434
        }
435
436 24
        foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
437
            /* Text indexes will always report defaults for these options, so
438
             * only compare if we have explicit values in the document index. */
439 24 View Code Duplication
            if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
440 24
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
441 24
                return false;
442
            }
443
        }
444
445 21
        return true;
446
    }
447
448
    /**
449
     * Determine if the keys for a MongoDB index can be considered equivalent to
450
     * those for an index in class metadata.
451
     *
452
     * @param array|IndexInfo $mongoIndex    Mongo index data.
453
     * @param array           $documentIndex Document index data.
454
     * @return bool True if the indexes have equivalent keys, otherwise false.
455
     */
456 47
    private function isEquivalentIndexKeys($mongoIndex, array $documentIndex)
457
    {
458 47
        $mongoIndexKeys    = $mongoIndex['key'];
459 47
        $documentIndexKeys = $documentIndex['keys'];
460
461
        /* If we are dealing with text indexes, we need to unset internal fields
462
         * from the MongoDB index and filter out text fields from the document
463
         * index. This will leave only non-text fields, which we can compare as
464
         * normal. Any text fields in the document index will be compared later
465
         * with isEquivalentTextIndexWeights(). */
466 47
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
467 15
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
468
469
            $documentIndexKeys = array_filter($documentIndexKeys, function ($type) {
470 15
                return $type !== 'text';
471 15
            });
472
        }
473
474
        /* Avoid a strict equality check here. The numeric type returned by
475
         * MongoDB may differ from the document index without implying that the
476
         * indexes themselves are inequivalent. */
477
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
478 47
        return $mongoIndexKeys == $documentIndexKeys;
479
    }
480
481
    /**
482
     * Determine if the text index weights for a MongoDB index can be considered
483
     * equivalent to those for an index in class metadata.
484
     *
485
     * @param array|IndexInfo $mongoIndex    Mongo index data.
486
     * @param array           $documentIndex Document index data.
487
     * @return bool True if the indexes have equivalent weights, otherwise false.
488
     */
489 14
    private function isEquivalentTextIndexWeights($mongoIndex, array $documentIndex)
490
    {
491 14
        $mongoIndexWeights    = $mongoIndex['weights'];
492 14
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
493
494
        // If not specified, assign a default weight for text fields
495 14
        foreach ($documentIndex['keys'] as $key => $type) {
496 14
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
497 5
                continue;
498
            }
499
500 9
            $documentIndexWeights[$key] = 1;
501
        }
502
503
        /* MongoDB returns the weights sorted by field name, but we'll sort both
504
         * arrays in case that is internal behavior not to be relied upon. */
505 14
        ksort($mongoIndexWeights);
506 14
        ksort($documentIndexWeights);
507
508
        /* Avoid a strict equality check here. The numeric type returned by
509
         * MongoDB may differ from the document index without implying that the
510
         * indexes themselves are inequivalent. */
511
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
512 14
        return $mongoIndexWeights == $documentIndexWeights;
513
    }
514
515
    /**
516
     * Ensure collections are sharded for all documents that can be loaded with the
517
     * metadata factory.
518
     *
519
     * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections
520
     *
521
     * @throws MongoDBException
522
     */
523
    public function ensureSharding(array $indexOptions = [])
524
    {
525
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
526
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
527
                continue;
528
            }
529
530
            $this->ensureDocumentSharding($class->name, $indexOptions);
531
        }
532
    }
533
534
    /**
535
     * Ensure sharding for collection by document name.
536
     *
537
     * @param string $documentName
538
     * @param array  $indexOptions Options for `ensureIndex` command. It's performed on an existing collections.
539
     *
540
     * @throws MongoDBException
541
     */
542 2
    public function ensureDocumentSharding($documentName, array $indexOptions = [])
543
    {
544 2
        $class = $this->dm->getClassMetadata($documentName);
545 2
        if (! $class->isSharded()) {
546
            return;
547
        }
548
549 2
        $this->enableShardingForDbByDocumentName($documentName);
550
551 2
        $try = 0;
552
        do {
553
            try {
554 2
                $result = $this->runShardCollectionCommand($documentName);
555 2
                $done = true;
556
557
                // Need to check error message because MongoDB 3.0 does not return a code for this error
558 2
                if (! (bool) $result['ok'] && strpos($result['errmsg'], 'please create an index that starts') !== false) {
559
                    // The proposed key is not returned when using mongo-php-adapter with ext-mongodb.
560
                    // See https://github.com/mongodb/mongo-php-driver/issues/296 for details
561
                    $key = $result['proposedKey'] ?? $this->dm->getClassMetadata($documentName)->getShardKey()['keys'];
562
563
                    $this->dm->getDocumentCollection($documentName)->ensureIndex($key, $indexOptions);
564 2
                    $done = false;
565
                }
566 1
            } catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
567 1
                if ($e->getCode() === 20 || $e->getCode() === 23 || $e->getMessage() === 'already sharded') {
568 1
                    return;
569
                }
570
571
                throw $e;
572
            }
573 2
        } while (! $done && $try < 2);
0 ignored issues
show
introduced by
It seems like the condition $try < 2 is always satisfied by any possible value of $try. Are you sure you do not have a deadlock here?
Loading history...
574
575
        // Starting with MongoDB 3.2, this command returns code 20 when a collection is already sharded.
576
        // For older MongoDB versions, check the error message
577 2
        if ((bool) $result['ok'] || (isset($result['code']) && $result['code'] === 20) || $result['errmsg'] === 'already sharded') {
578 2
            return;
579
        }
580
581
        throw MongoDBException::failedToEnsureDocumentSharding($documentName, $result['errmsg']);
582
    }
583
584
    /**
585
     * Enable sharding for database which contains documents with given name.
586
     *
587
     * @param string $documentName
588
     *
589
     * @throws MongoDBException
590
     */
591 2
    public function enableShardingForDbByDocumentName($documentName)
592
    {
593 2
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
594 2
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
595
596
        try {
597 2
            $adminDb->command(['enableSharding' => $dbName]);
598 1
        } catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
599 1
            if ($e->getCode() !== 23 || $e->getMessage() === 'already enabled') {
600
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
601
            }
602
        }
603 2
    }
604
605
    /**
606
     * @param string $documentName
607
     *
608
     * @return array
609
     */
610 2
    private function runShardCollectionCommand($documentName)
611
    {
612 2
        $class = $this->dm->getClassMetadata($documentName);
613 2
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
614 2
        $shardKey = $class->getShardKey();
615 2
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
616
617 2
        $result = $adminDb->command(
618
            [
619 2
                'shardCollection' => $dbName . '.' . $class->getCollection(),
620 2
                'key'             => $shardKey['keys'],
621
            ]
622 2
        )->toArray()[0];
623
624 2
        return $result;
625
    }
626
}
627