Completed
Pull Request — master (#1910)
by Andreas
24:45
created

SchemaManager::isEquivalentIndexOptions()   C

Complexity

Conditions 15
Paths 16

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 15

Importance

Changes 0
Metric Value
dl 0
loc 50
c 0
b 0
f 0
ccs 19
cts 19
cp 1
rs 5.9166
cc 15
nc 16
nop 2
crap 15

How to fix   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 InvalidArgumentException;
10
use MongoDB\Driver\Exception\RuntimeException;
11
use MongoDB\Driver\Exception\ServerException;
12
use MongoDB\Driver\WriteConcern;
13
use MongoDB\Model\IndexInfo;
14
use function array_diff_key;
15
use function array_filter;
16
use function array_unique;
17
use function iterator_count;
18
use function iterator_to_array;
19
use function ksort;
20
21
class SchemaManager
22
{
23
    private const GRIDFS_FILE_COLLECTION_INDEX = ['files_id' => 1, 'n' => 1];
24
25
    private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1];
26
27
    private const CODE_SHARDING_ALREADY_INITIALIZED = 23;
28
29
    private const ALLOWED_MISSING_INDEX_OPTIONS = ['partialFilterExpression'];
30
31
    /** @var DocumentManager */
32
    protected $dm;
33 1705
34
    /** @var ClassMetadataFactory */
35 1705
    protected $metadataFactory;
36 1705
37 1705
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
38
    {
39
        $this->dm              = $dm;
40
        $this->metadataFactory = $cmf;
41
    }
42
43 1
    /**
44
     * Ensure indexes are created for all documents that can be loaded with the
45 1
     * metadata factory.
46 1
     */
47 1
    public function ensureIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
48
    {
49
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
50 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
51
                continue;
52 1
            }
53
54
            $this->ensureDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
55
        }
56
    }
57
58
    /**
59
     * Ensure indexes exist for all mapped document classes.
60
     *
61
     * Indexes that exist in MongoDB but not the document metadata will be
62
     * deleted.
63
     */
64
    public function updateIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
65
    {
66
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
67
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
68
                continue;
69
            }
70
71
            $this->updateDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
72
        }
73
    }
74
75
    /**
76
     * Ensure indexes exist for the mapped document class.
77
     *
78
     * Indexes that exist in MongoDB but not the document metadata will be
79 3
     * deleted.
80
     *
81 3
     * @throws InvalidArgumentException
82
     */
83 3
    public function updateDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
84
    {
85
        $class = $this->dm->getClassMetadata($documentName);
86
87 3
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
88 3
            throw new InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.');
89 3
        }
90
91
        $documentIndexes = $this->getDocumentIndexes($documentName);
92
        $collection      = $this->dm->getDocumentCollection($documentName);
93
        $mongoIndexes    = iterator_to_array($collection->listIndexes());
94 3
95
        /* Determine which Mongo indexes should be deleted. Exclude the ID index
96 1
         * and those that are equivalent to any in the class metadata.
97
         */
98
        $self         = $this;
99
        $mongoIndexes = array_filter($mongoIndexes, static function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
100 1
            if ($mongoIndex['name'] === '_id_') {
101 1
                return false;
102 1
            }
103
104
            foreach ($documentIndexes as $documentIndex) {
105
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
106 1
                    return false;
107 3
                }
108
            }
109
110 3
            return true;
111 1
        });
112
113
        // Delete indexes that do not exist in class metadata
114
        foreach ($mongoIndexes as $mongoIndex) {
115 1
            if (! isset($mongoIndex['name'])) {
116
                continue;
117
            }
118 3
119 3
            $collection->dropIndex($mongoIndex['name'], $this->getWriteOptions($maxTimeMs, $writeConcern));
120
        }
121 23
122
        $this->ensureDocumentIndexes($documentName, $maxTimeMs, $writeConcern);
123 23
    }
124 23
125
    public function getDocumentIndexes(string $documentName) : array
126
    {
127 23
        $visited = [];
128
        return $this->doGetDocumentIndexes($documentName, $visited);
129 23
    }
130 1
131
    private function doGetDocumentIndexes(string $documentName, array &$visited) : array
132
    {
133 23
        if (isset($visited[$documentName])) {
134
            return [];
135 23
        }
136 23
137 23
        $visited[$documentName] = true;
138
139
        $class                   = $this->dm->getClassMetadata($documentName);
140 23
        $indexes                 = $this->prepareIndexes($class);
141 23
        $embeddedDocumentIndexes = [];
142 4
143 4
        // Add indexes from embedded & referenced documents
144 2
        foreach ($class->fieldMappings as $fieldMapping) {
145 1
            if (isset($fieldMapping['embedded'])) {
146
                if (isset($fieldMapping['targetDocument'])) {
147 1
                    $possibleEmbeds = [$fieldMapping['targetDocument']];
148
                } elseif (isset($fieldMapping['discriminatorMap'])) {
149
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
150 4
                } else {
151 4
                    continue;
152 2
                }
153
154 4
                foreach ($possibleEmbeds as $embed) {
155 4
                    if (isset($embeddedDocumentIndexes[$embed])) {
156
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
157
                    } else {
158 4
                        $embeddedIndexes                 = $this->doGetDocumentIndexes($embed, $visited);
159 2
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
160 2
                    }
161 2
162
                    foreach ($embeddedIndexes as $embeddedIndex) {
163
                        foreach ($embeddedIndex['keys'] as $key => $value) {
164 4
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
165
                            unset($embeddedIndex['keys'][$key]);
166
                        }
167 23
168 11
                        $indexes[] = $embeddedIndex;
169 10
                    }
170 10
                }
171 10
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
172 2
                foreach ($indexes as $idx => $index) {
173
                    $newKeys = [];
174
                    foreach ($index['keys'] as $key => $v) {
175 10
                        if ($key === $fieldMapping['name']) {
176
                            $key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key);
177
                        }
178 23
179
                        $newKeys[$key] = $v;
180
                    }
181
182
                    $indexes[$idx]['keys'] = $newKeys;
183 23
                }
184
            }
185
        }
186 23
187
        return $indexes;
188 23
    }
189 23
190 23
    private function prepareIndexes(ClassMetadata $class) : array
191
    {
192 23
        $persister  = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
193
        $indexes    = $class->getIndexes();
194 22
        $newIndexes = [];
195 22
196
        foreach ($indexes as $index) {
197
            $newIndex = [
198 22
                'keys' => [],
199 22
                'options' => $index['options'],
200 22
            ];
201 17
202 17
            foreach ($index['keys'] as $key => $value) {
203
                $key = $persister->prepareFieldName($key);
204 22
                if ($class->hasField($key)) {
205
                    $mapping                            = $class->getFieldMapping($key);
206
                    $newIndex['keys'][$mapping['name']] = $value;
207
                } else {
208 22
                    $newIndex['keys'][$key] = $value;
209
                }
210
            }
211 23
212
            $newIndexes[] = $newIndex;
213
        }
214
215
        return $newIndexes;
216
    }
217
218
    /**
219 19
     * Ensure the given document's indexes are created.
220
     *
221 19
     * @throws InvalidArgumentException
222 19
     */
223
    public function ensureDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
224
    {
225
        $class = $this->dm->getClassMetadata($documentName);
226 19
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
227 2
            throw new InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
228
        }
229
230 19
        if ($class->isFile) {
231 19
            $this->ensureGridFSIndexes($class, $maxTimeMs, $writeConcern);
232 4
        }
233
234
        $indexes = $this->getDocumentIndexes($documentName);
235 18
        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...
236 18
            return;
237 18
        }
238 18
239
        $collection = $this->dm->getDocumentCollection($class->name);
240 18
        foreach ($indexes as $index) {
241 1
            $collection->createIndex($index['keys'], $this->getWriteOptions($maxTimeMs, $writeConcern, $index['options']));
242
        }
243
    }
244 18
245
    /**
246 18
     * Delete indexes for all documents that can be loaded with the
247
     * metadata factory.
248
     */
249
    public function deleteIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
250
    {
251
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
252 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
253
                continue;
254 1
            }
255 1
256 1
            $this->deleteDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
257
        }
258
    }
259 1
260
    /**
261 1
     * Delete the given document's indexes.
262
     *
263
     * @throws InvalidArgumentException
264
     */
265
    public function deleteDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
266
    {
267
        $class = $this->dm->getClassMetadata($documentName);
268 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
269
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
270 2
        }
271 2
272
        $this->dm->getDocumentCollection($documentName)->dropIndexes($this->getWriteOptions($maxTimeMs, $writeConcern));
273
    }
274
275 2
    /**
276 2
     * Create all the mapped document collections in the metadata factory.
277
     */
278
    public function createCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
279
    {
280
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
281 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
282
                continue;
283 1
            }
284 1
            $this->createDocumentCollection($class->name, $maxTimeMs, $writeConcern);
285 1
        }
286
    }
287 1
288
    /**
289 1
     * Create the document collection for a mapped class.
290
     *
291
     * @throws InvalidArgumentException
292
     */
293
    public function createDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
294
    {
295
        $class = $this->dm->getClassMetadata($documentName);
296 5
297
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
298 5
            throw new InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
299
        }
300 5
301
        if ($class->isFile) {
302
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
303
304 5
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files', $options);
305 2
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks', $options);
306 2
307
            return;
308 2
        }
309
310
        $options = [
311 4
            'capped' => $class->getCollectionCapped(),
312 4
            'size' => $class->getCollectionSize(),
313
            'max' => $class->getCollectionMax(),
314 4
        ];
315 4
316 4
        $this->dm->getDocumentDatabase($documentName)->createCollection(
317
            $class->getCollection(),
318
            $this->getWriteOptions($maxTimeMs, $writeConcern, $options)
319 4
        );
320
    }
321
322
    /**
323
     * Drop all the mapped document collections in the metadata factory.
324 1
     */
325
    public function dropCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
326 1
    {
327 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
328 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
329
                continue;
330
            }
331 1
332
            $this->dropDocumentCollection($class->name, $maxTimeMs, $writeConcern);
333 1
        }
334
    }
335
336
    /**
337
     * Drop the document collection for a mapped class.
338
     *
339
     * @throws InvalidArgumentException
340 5
     */
341
    public function dropDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
342 5
    {
343 5
        $class = $this->dm->getClassMetadata($documentName);
344
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
345
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
346
        }
347 5
348
        $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
349 5
350 4
        $this->dm->getDocumentCollection($documentName)->drop($options);
351
352
        if (! $class->isFile) {
353 2
            return;
354 2
        }
355
356
        $this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop($options);
357
    }
358
359 1
    /**
360
     * Drop all the mapped document databases in the metadata factory.
361 1
     */
362 1
    public function dropDatabases(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
363 1
    {
364
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
365
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
366 1
                continue;
367
            }
368 1
369
            $this->dropDocumentDatabase($class->name, $maxTimeMs, $writeConcern);
370
        }
371
    }
372
373
    /**
374
     * Drop the document database for a mapped class.
375 2
     *
376
     * @throws InvalidArgumentException
377 2
     */
378 2
    public function dropDocumentDatabase(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
379
    {
380
        $class = $this->dm->getClassMetadata($documentName);
381
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
382 2
            throw new InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
383 2
        }
384
385
        $this->dm->getDocumentDatabase($documentName)->drop($this->getWriteOptions($maxTimeMs, $writeConcern));
386
    }
387
388
    /**
389
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
390
     * considered equivalent to an index in class metadata.
391
     *
392
     * Indexes are considered different if:
393
     *
394
     *   (a) Key/direction pairs differ or are not in the same order
395
     *   (b) Sparse or unique options differ
396
     *   (c) Geospatial options differ (bits, max, min)
397
     *   (d) The partialFilterExpression differs
398
     *
399
     * The background option is only relevant to index creation and is not
400
     * considered.
401
     */
402
    public function isMongoIndexEquivalentToDocumentIndex(IndexInfo $mongoIndex, array $documentIndex) : bool
403
    {
404
        return $this->isEquivalentIndexKeys($mongoIndex, $documentIndex) && $this->isEquivalentIndexOptions($mongoIndex, $documentIndex);
405 47
    }
406
407 47
    /**
408
     * Determine if the keys for a MongoDB index can be considered equivalent to
409 47
     * those for an index in class metadata.
410 4
     */
411
    private function isEquivalentIndexKeys(IndexInfo $mongoIndex, array $documentIndex) : bool
412
    {
413 43
        $mongoIndexKeys    = $mongoIndex['key'];
414 2
        $documentIndexKeys = $documentIndex['keys'];
415
416
        /* If we are dealing with text indexes, we need to unset internal fields
417 41
         * from the MongoDB index and filter out text fields from the document
418 2
         * index. This will leave only non-text fields, which we can compare as
419
         * normal. Any text fields in the document index will be compared later
420
         * with isEquivalentTextIndexWeights(). */
421 39
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
422 39
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
423 1
424
            $documentIndexKeys = array_filter($documentIndexKeys, static function ($type) {
425
                return $type !== 'text';
426 38
            });
427 38
        }
428 6
429
        /* Avoid a strict equality check here. The numeric type returned by
430
         * MongoDB may differ from the document index without implying that the
431 36
         * indexes themselves are inequivalent. */
432 36
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
433 36
        return $mongoIndexKeys == $documentIndexKeys;
434
    }
435
436
    private function isEquivalentIndexOptions(IndexInfo $mongoIndex, array $documentIndex) : bool
437 29
    {
438 2
        $mongoIndexOptions = $mongoIndex->__debugInfo();
439
        unset($mongoIndexOptions['v'], $mongoIndexOptions['ns'], $mongoIndexOptions['key']);
440
441 27
        $documentIndexOptions = $documentIndex['options'];
442 27
443 1
        if ($this->indexOptionsAreMissing($mongoIndexOptions, $documentIndexOptions)) {
444
            return false;
445
        }
446 26
447 2
        if (empty($mongoIndexOptions['sparse']) xor empty($documentIndexOptions['sparse'])) {
448
            return false;
449
        }
450 24
451
        if (empty($mongoIndexOptions['unique']) xor empty($documentIndexOptions['unique'])) {
452
            return false;
453 24
        }
454 24
455 24
        foreach (['bits', 'max', 'min'] as $option) {
456
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
457
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
458
                return false;
459 21
            }
460
        }
461
462
        if (empty($mongoIndexOptions['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
463
            return false;
464
        }
465
466
        if (isset($mongoIndexOptions['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
467
            $mongoIndexOptions['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
468 47
            return false;
469
        }
470 47
471 47
        if (isset($mongoIndexOptions['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
472
            return false;
473
        }
474
475
        foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
476
            /* Text indexes will always report defaults for these options, so
477
             * only compare if we have explicit values in the document index. */
478 47
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
479 15
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
480
                return false;
481
            }
482 15
        }
483 15
484
        return true;
485
    }
486
487
    /**
488
     * Checks if any index options are missing.
489
     *
490 47
     * Options added to the ALLOWED_MISSING_INDEX_OPTIONS constant are ignored
491
     * and are expected to be checked later
492
     */
493
    private function indexOptionsAreMissing(array $mongoIndexOptions, array $documentIndexOptions) : bool
494
    {
495
        foreach (self::ALLOWED_MISSING_INDEX_OPTIONS as $option) {
496
            unset($mongoIndexOptions[$option], $documentIndexOptions[$option]);
497
        }
498
499 14
        return array_diff_key($mongoIndexOptions, $documentIndexOptions) !== [] || array_diff_key($documentIndexOptions, $mongoIndexOptions) !== [];
500
    }
501 14
502 14
    /**
503
     * Determine if the text index weights for a MongoDB index can be considered
504
     * equivalent to those for an index in class metadata.
505 14
     */
506 14
    private function isEquivalentTextIndexWeights(IndexInfo $mongoIndex, array $documentIndex) : bool
507 5
    {
508
        $mongoIndexWeights    = $mongoIndex['weights'];
509
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
510 9
511
        // If not specified, assign a default weight for text fields
512
        foreach ($documentIndex['keys'] as $key => $type) {
513
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
514
                continue;
515 14
            }
516 14
517
            $documentIndexWeights[$key] = 1;
518
        }
519
520
        /* MongoDB returns the weights sorted by field name, but we'll sort both
521
         * arrays in case that is internal behavior not to be relied upon. */
522 14
        ksort($mongoIndexWeights);
523
        ksort($documentIndexWeights);
524
525
        /* Avoid a strict equality check here. The numeric type returned by
526
         * MongoDB may differ from the document index without implying that the
527
         * indexes themselves are inequivalent. */
528
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
529
        return $mongoIndexWeights == $documentIndexWeights;
530
    }
531
532
    /**
533
     * Ensure collections are sharded for all documents that can be loaded with the
534
     * metadata factory.
535
     *
536
     * @throws MongoDBException
537
     */
538
    public function ensureSharding() : void
539
    {
540
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
541
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
542
                continue;
543
            }
544
545
            $this->ensureDocumentSharding($class->name);
546
        }
547 11
    }
548
549 11
    /**
550 11
     * Ensure sharding for collection by document name.
551
     *
552
     * @throws MongoDBException
553
     */
554 11
    public function ensureDocumentSharding(string $documentName) : void
555 1
    {
556
        $class = $this->dm->getClassMetadata($documentName);
557
        if (! $class->isSharded()) {
558 11
            return;
559
        }
560
561 11
        if ($this->collectionIsSharded($documentName)) {
562 1
            return;
563 1
        }
564
565 10
        $this->enableShardingForDbByDocumentName($documentName);
566
567
        try {
568
            $this->runShardCollectionCommand($documentName);
569
        } 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...
570
            throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage());
571
        }
572 11
    }
573
574 11
    /**
575 11
     * Enable sharding for database which contains documents with given name.
576
     *
577
     * @throws MongoDBException
578 11
     */
579 10
    public function enableShardingForDbByDocumentName(string $documentName) : void
580
    {
581 10
        $dbName  = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
582 10
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
583
584
        try {
585
            $adminDb->command(['enableSharding' => $dbName]);
586
        } catch (ServerException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\ServerException 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...
587 11
            // Don't throw an exception if sharding is already enabled; there's just no other way to check this
588
            if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) {
589 11
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
590
            }
591 11
        } 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...
592 11
            throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
593 11
        }
594 11
    }
595
596 11
    private function runShardCollectionCommand(string $documentName) : array
597
    {
598 11
        $class    = $this->dm->getClassMetadata($documentName);
599 11
        $dbName   = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
600
        $shardKey = $class->getShardKey();
601 10
        $adminDb  = $this->dm->getClient()->selectDatabase('admin');
602
603
        return $adminDb->command(
604 2
            [
605
                'shardCollection' => $dbName . '.' . $class->getCollection(),
606 2
                'key'             => $shardKey['keys'],
607 2
            ]
608 2
        )->toArray()[0];
609
    }
610 2
611
    private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
612 2
    {
613 2
        $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern);
614
        $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern);
615
    }
616
617
    private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
618
    {
619 2
        $chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection();
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
620 2
        foreach ($chunksCollection->listIndexes() as $index) {
621
            if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) {
622 2
                return;
623
            }
624 2
        }
625 2
626
        $chunksCollection->createIndex(
627
            self::GRIDFS_FILE_COLLECTION_INDEX,
628
            $this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true])
629
        );
630
    }
631 2
632 2
    private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
633
    {
634 11
        $filesCollection = $this->dm->getDocumentCollection($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
635
        foreach ($filesCollection->listIndexes() as $index) {
636 11
            if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) {
637
                return;
638 11
            }
639 11
        }
640 11
641 7
        $filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern));
642
    }
643
644 4
    private function collectionIsSharded(string $documentName) : bool
645 4
    {
646
        $class = $this->dm->getClassMetadata($documentName);
647
648
        $database    = $this->dm->getDocumentDatabase($documentName);
649
        $collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]);
650
        if (! iterator_count($collections)) {
651
            return false;
652
        }
653
654
        $stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0];
655
        return (bool) ($stats['sharded'] ?? false);
656
    }
657
658
    private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []): array
659
    {
660
        unset($options['maxTimeMs'], $options['writeConcern']);
661
662
        if ($maxTimeMs !== null) {
663
            $options['maxTimeMs'] = $maxTimeMs;
664
        }
665
666
        if ($writeConcern !== null) {
667
            $options['writeConcern'] = $writeConcern;
668
        }
669
670
        return $options;
671
    }
672
}
673