Completed
Pull Request — master (#1911)
by Andreas
22:44
created

SchemaManager::isEquivalentIndexOptions()   C

Complexity

Conditions 15
Paths 16

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 15

Importance

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