SchemaManager::dropDocumentDatabase()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4.074

Importance

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