Completed
Push — master ( 8de00c...183805 )
by Jeremy
13s
created

isMongoIndexEquivalentToDocumentIndex()   D

Complexity

Conditions 20
Paths 18

Size

Total Lines 56

Duplication

Lines 8
Ratio 14.29 %

Code Coverage

Tests 22
CRAP Score 21.4552

Importance

Changes 0
Metric Value
dl 8
loc 56
ccs 22
cts 26
cp 0.8462
rs 4.1666
c 0
b 0
f 0
cc 20
nc 18
nop 2
crap 21.4552

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 1627
25
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
26 1627
    {
27 1627
        $this->dm = $dm;
28 1627
        $this->metadataFactory = $cmf;
29
    }
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 1
     */
37 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 1
    {
39 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
40 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
41
                continue;
42 1
            }
43
            $this->ensureDocumentIndexes($class->name, $timeout);
44 1
        }
45
    }
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 3
     */
75
    public function updateDocumentIndexes($documentName, $timeout = null)
76 3
    {
77
        $class = $this->dm->getClassMetadata($documentName);
78 3
79
        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 3
83 3
        $documentIndexes = $this->getDocumentIndexes($documentName);
84 3
        $collection = $this->dm->getDocumentCollection($documentName);
85
        $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 3
         */
90
        $self = $this;
91 1
        $mongoIndexes = array_filter($mongoIndexes, function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
92
            if ($mongoIndex['name'] === '_id_') {
93
                return false;
94
            }
95 1
96 1
            foreach ($documentIndexes as $documentIndex) {
97 1
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
98
                    return false;
99
                }
100
            }
101 1
102 3
            return true;
103
        });
104
105 3
        // Delete indexes that do not exist in class metadata
106 1
        foreach ($mongoIndexes as $mongoIndex) {
107
            if (! isset($mongoIndex['name'])) {
108
                continue;
109
            }
110 1
111
            $collection->dropIndex($mongoIndex['name']);
112
        }
113 3
114 3
        $this->ensureDocumentIndexes($documentName, $timeout);
115
    }
116
117
    /**
118
     * @param string $documentName
119
     * @return array
120 19
     */
121
    public function getDocumentIndexes($documentName)
122 19
    {
123 19
        $visited = [];
124
        return $this->doGetDocumentIndexes($documentName, $visited);
125
    }
126
127
    /**
128
     * @param string $documentName
129
     * @param array  $visited
130
     * @return array
131 19
     */
132
    private function doGetDocumentIndexes($documentName, array &$visited)
133 19
    {
134 1
        if (isset($visited[$documentName])) {
135
            return [];
136
        }
137 19
138
        $visited[$documentName] = true;
139 19
140 19
        $class = $this->dm->getClassMetadata($documentName);
141 19
        $indexes = $this->prepareIndexes($class);
142
        $embeddedDocumentIndexes = [];
143
144 19
        // Add indexes from embedded & referenced documents
145 19
        foreach ($class->fieldMappings as $fieldMapping) {
146 3
            if (isset($fieldMapping['embedded'])) {
147 3
                if (isset($fieldMapping['targetDocument'])) {
148 2
                    $possibleEmbeds = [$fieldMapping['targetDocument']];
149 1
                } elseif (isset($fieldMapping['discriminatorMap'])) {
150
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
151 1
                } else {
152
                    continue;
153 3
                }
154 3
                foreach ($possibleEmbeds as $embed) {
155 2
                    if (isset($embeddedDocumentIndexes[$embed])) {
156
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
157 3
                    } else {
158 3
                        $embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited);
159
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
160 3
                    }
161 2
                    foreach ($embeddedIndexes as $embeddedIndex) {
162 2
                        foreach ($embeddedIndex['keys'] as $key => $value) {
163 2
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
164
                            unset($embeddedIndex['keys'][$key]);
165 3
                        }
166
                        $indexes[] = $embeddedIndex;
167
                    }
168 19
                }
169 8
            } 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 2
                        if ($key === $fieldMapping['name']) {
174
                            $key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key);
175 8
                        }
176
                        $newKeys[$key] = $v;
177 19
                    }
178
                    $indexes[$idx]['keys'] = $newKeys;
179
                }
180
            }
181 19
        }
182
        return $indexes;
183
    }
184
185
    /**
186
     * @return array
187 19
     */
188
    private function prepareIndexes(ClassMetadata $class)
189 19
    {
190 19
        $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
191 19
        $indexes = $class->getIndexes();
192
        $newIndexes = [];
193 19
194
        foreach ($indexes as $index) {
195 19
            $newIndex = [
196 19
                'keys' => [],
197
                'options' => $index['options'],
198 19
            ];
199 19
            foreach ($index['keys'] as $key => $value) {
200 19
                $key = $persister->prepareFieldName($key);
201 17
                if ($class->hasField($key)) {
202 17
                    $mapping = $class->getFieldMapping($key);
203
                    $newIndex['keys'][$mapping['name']] = $value;
204 19
                } else {
205
                    $newIndex['keys'][$key] = $value;
206
                }
207
            }
208 19
209
            $newIndexes[] = $newIndex;
210
        }
211 19
212
        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 15
     */
222
    public function ensureDocumentIndexes($documentName, $timeout = null)
223 15
    {
224 15
        $class = $this->dm->getClassMetadata($documentName);
225
        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 15
229 15
        $indexes = $this->getDocumentIndexes($documentName);
230 3
        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
            return;
232
        }
233 15
234 15
        $collection = $this->dm->getDocumentCollection($class->name);
235 15
        foreach ($indexes as $index) {
236 15
            $keys = $index['keys'];
237
            $options = $index['options'];
238 15
239 1
            if (! isset($options['timeout']) && isset($timeout)) {
240
                $options['timeout'] = $timeout;
241
            }
242 15
243
            $collection->createIndex($keys, $options);
244 15
        }
245
    }
246
247
    /**
248
     * Delete indexes for all documents that can be loaded with the
249
     * metadata factory.
250 1
     */
251 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 1
    {
253 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
254 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
255
                continue;
256 1
            }
257
            $this->deleteDocumentIndexes($class->name);
258 1
        }
259
    }
260
261
    /**
262
     * Delete the given document's indexes.
263
     *
264
     * @param string $documentName
265
     * @throws \InvalidArgumentException
266 2
     */
267 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 2
    {
269 2
        $class = $this->dm->getClassMetadata($documentName);
270
        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 2
        }
273 2
        $this->dm->getDocumentCollection($documentName)->dropIndexes();
274
    }
275
276
    /**
277
     * Create all the mapped document collections in the metadata factory.
278 1
     */
279 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 1
    {
281 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
282 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
283
                continue;
284 1
            }
285
            $this->createDocumentCollection($class->name);
286 1
        }
287
    }
288
289
    /**
290
     * Create the document collection for a mapped class.
291
     *
292
     * @param string $documentName
293
     * @throws \InvalidArgumentException
294 4
     */
295
    public function createDocumentCollection($documentName)
296 4
    {
297
        $class = $this->dm->getClassMetadata($documentName);
298 4
299
        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 4
303 4
        $this->dm->getDocumentDatabase($documentName)->createCollection(
304
            $class->getCollection(),
305 4
            [
306 4
                'capped' => $class->getCollectionCapped(),
307 4
                'size' => $class->getCollectionSize(),
308
                'max' => $class->getCollectionMax(),
309
            ]
310 4
        );
311
    }
312
313
    /**
314
     * Drop all the mapped document collections in the metadata factory.
315 1
     */
316 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 1
    {
318 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
319 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
320
                continue;
321 1
            }
322
            $this->dropDocumentCollection($class->name);
323 1
        }
324
    }
325
326
    /**
327
     * Drop the document collection for a mapped class.
328
     *
329
     * @param string $documentName
330
     * @throws \InvalidArgumentException
331 4
     */
332 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 4
    {
334 4
        $class = $this->dm->getClassMetadata($documentName);
335
        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 4
        }
338 4
        $this->dm->getDocumentCollection($documentName)->drop();
339
    }
340
341
    /**
342
     * Drop all the mapped document databases in the metadata factory.
343 1
     */
344 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 1
    {
346 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
347 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
348
                continue;
349 1
            }
350
            $this->dropDocumentDatabase($class->name);
351 1
        }
352
    }
353
354
    /**
355
     * Drop the document database for a mapped class.
356
     *
357
     * @param string $documentName
358
     * @throws \InvalidArgumentException
359 2
     */
360 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 2
    {
362 2
        $class = $this->dm->getClassMetadata($documentName);
363
        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 2
        }
366 2
        $this->dm->getDocumentDatabase($documentName)->drop();
367
    }
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 30
     */
391
    public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)
392 30
    {
393
        $documentIndexOptions = $documentIndex['options'];
394 30
395 2
        if (! $this->isEquivalentIndexKeys($mongoIndex, $documentIndex)) {
396
            return false;
397
        }
398 28
399 2
        if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) {
400
            return false;
401
        }
402 26
403 2
        if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) {
404
            return false;
405
        }
406 24
407 24
        if (! empty($mongoIndex['unique']) && empty($mongoIndex['dropDups']) &&
408 1
            ! empty($documentIndexOptions['unique']) && ! empty($documentIndexOptions['dropDups'])) {
409
            return false;
410
        }
411 23
412 23
        foreach (['bits', 'max', 'min'] as $option) {
413 6
            if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) {
414
                return false;
415
            }
416 21
417 21 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 21
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
419
                return false;
420
            }
421
        }
422 14
423 2
        if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
424
            return false;
425
        }
426 12
427 12
        if (isset($mongoIndex['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
428 1
            $mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
429
            return false;
430
        }
431 11
432
        if (isset($mongoIndex['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
433
            return false;
434
        }
435
436
        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 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
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
441
                return false;
442
            }
443
        }
444
445
        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
    private function isEquivalentIndexKeys($mongoIndex, array $documentIndex)
457
    {
458
        $mongoIndexKeys    = $mongoIndex['key'];
459
        $documentIndexKeys = $documentIndex['keys'];
460
461 2
        /* 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 2
         * index. This will leave only non-text fields, which we can compare as
464 2
         * normal. Any text fields in the document index will be compared later
465
         * with isEquivalentTextIndexWeights(). */
466
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
467
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
468 2
469
            $documentIndexKeys = array_filter($documentIndexKeys, function ($type) {
470 2
                return $type !== 'text';
471
            });
472
        }
473 2
474 2
        /* 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 2
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
478
        return $mongoIndexKeys == $documentIndexKeys;
479
    }
480
481
    /**
482
     * Determine if the text index weights for a MongoDB index can be considered
483 2
     * equivalent to those for an index in class metadata.
484
     *
485 1
     * @param array|IndexInfo $mongoIndex    Mongo index data.
486 1
     * @param array           $documentIndex Document index data.
487 1
     * @return bool True if the indexes have equivalent weights, otherwise false.
488
     */
489
    private function isEquivalentTextIndexWeights($mongoIndex, array $documentIndex)
490
    {
491
        $mongoIndexWeights    = $mongoIndex['weights'];
492 2
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
493
494
        // If not specified, assign a default weight for text fields
495
        foreach ($documentIndex['keys'] as $key => $type) {
496 2
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
497 2
                continue;
498
            }
499
500
            $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
        ksort($mongoIndexWeights);
506
        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 2
         * indexes themselves are inequivalent. */
511
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
512 2
        return $mongoIndexWeights == $documentIndexWeights;
513 2
    }
514
515
    /**
516 2
     * Ensure collections are sharded for all documents that can be loaded with the
517 1
     * metadata factory.
518 1
     *
519
     * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections
520
     *
521
     * @throws MongoDBException
522 2
     */
523
    public function ensureSharding(array $indexOptions = [])
524
    {
525
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
526
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
527
                continue;
528
            }
529 2
530
            $this->ensureDocumentSharding($class->name, $indexOptions);
531 2
        }
532 2
    }
533 2
534 2
    /**
535
     * Ensure sharding for collection by document name.
536 2
     *
537
     * @param string $documentName
538 2
     * @param array  $indexOptions Options for `ensureIndex` command. It's performed on an existing collections.
539 2
     *
540
     * @throws MongoDBException
541 2
     */
542
    public function ensureDocumentSharding($documentName, array $indexOptions = [])
543 2
    {
544
        $class = $this->dm->getClassMetadata($documentName);
545
        if (! $class->isSharded()) {
546
            return;
547
        }
548
549
        $this->enableShardingForDbByDocumentName($documentName);
550
551
        $try = 0;
552
        do {
553
            try {
554
                $result = $this->runShardCollectionCommand($documentName);
555
                $done = true;
556
557
                // Need to check error message because MongoDB 3.0 does not return a code for this error
558
                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
                    $done = false;
565
                }
566
            } 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
                if ($e->getCode() === 20 || $e->getCode() === 23 || $e->getMessage() === 'already sharded') {
568
                    return;
569
                }
570
571
                throw $e;
572
            }
573
        } 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
        if ((bool) $result['ok'] || (isset($result['code']) && $result['code'] === 20) || $result['errmsg'] === 'already sharded') {
578
            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
    public function enableShardingForDbByDocumentName($documentName)
592
    {
593
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
594
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
595
596
        try {
597
            $adminDb->command(['enableSharding' => $dbName]);
598
        } 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
            if ($e->getCode() !== 23 || $e->getMessage() === 'already enabled') {
600
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
601
            }
602
        }
603
    }
604
605
    /**
606
     * @param string $documentName
607
     *
608
     * @return array
609
     */
610
    private function runShardCollectionCommand($documentName)
611
    {
612
        $class = $this->dm->getClassMetadata($documentName);
613
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
614
        $shardKey = $class->getShardKey();
615
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
616
617
        $result = $adminDb->command(
618
            [
619
                'shardCollection' => $dbName . '.' . $class->getCollection(),
620
                'key'             => $shardKey['keys'],
621
            ]
622
        )->toArray()[0];
623
624
        return $result;
625
    }
626
}
627