Completed
Pull Request — master (#2166)
by Andreas
18:35
created

SchemaManager::dropCollections()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 16
c 0
b 0
f 0
ccs 10
cts 10
cp 1
rs 9.1111
cc 6
nc 6
nop 2
crap 6
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 1821
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
53
    {
54 1821
        $this->dm              = $dm;
55 1821
        $this->metadataFactory = $cmf;
56 1821
    }
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
306
            switch (true) {
307 4
                case $class->isMappedSuperclass:
308 4
                case $class->isEmbeddedDocument:
309 4
                case $class->isQueryResultDocument:
310 4
                    continue 2;
311
312 4
                case $class->isView():
313
                default:
314 4
                    $this->createDocumentCollection($class->name, $maxTimeMs, $writeConcern);
315
            }
316
        }
317 4
    }
318
319
    /**
320
     * Create the document collection for a mapped class.
321
     *
322
     * @throws InvalidArgumentException
323
     */
324 19
    public function createDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
325
    {
326 19
        $class = $this->dm->getClassMetadata($documentName);
327
328 19
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
329
            throw new InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
330
        }
331
332 19
        if ($class->isView()) {
333 9
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
334
335 9
            $rootClass = $class->getRootClass();
336 9
            assert(is_string($rootClass));
337
338 9
            $builder    = $this->dm->createAggregationBuilder($rootClass);
339 9
            $repository = $this->dm->getRepository($class->name);
340 9
            assert($repository instanceof ViewRepository);
341 9
            $repository->createViewAggregation($builder);
342
343 9
            $collectionName = $this->dm->getDocumentCollection($rootClass)->getCollectionName();
344 9
            $this->dm->getDocumentDatabase($documentName)
345 9
                ->command([
346 9
                    'create' => $class->collection,
347 9
                    'viewOn' => $collectionName,
348 9
                    'pipeline' => $builder->getPipeline(),
349 9
                ], $options);
350
351 9
            return;
352
        }
353
354 14
        if ($class->isFile) {
355 8
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
356
357 8
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files', $options);
358 8
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks', $options);
359
360 8
            return;
361
        }
362
363
        $options = [
364 10
            'capped' => $class->getCollectionCapped(),
365 10
            'size' => $class->getCollectionSize(),
366 10
            'max' => $class->getCollectionMax(),
367
        ];
368
369 10
        $this->dm->getDocumentDatabase($documentName)->createCollection(
370 10
            $class->getCollection(),
371 10
            $this->getWriteOptions($maxTimeMs, $writeConcern, $options)
372
        );
373 10
    }
374
375
    /**
376
     * Drop all the mapped document collections in the metadata factory.
377
     */
378 4
    public function dropCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
379
    {
380 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
381 4
            assert($class instanceof ClassMetadata);
382
            switch (true) {
383 4
                case $class->isMappedSuperclass:
384 4
                case $class->isEmbeddedDocument:
385 4
                case $class->isQueryResultDocument:
386 4
                    continue 2;
387
388 4
                case $class->isView():
389
                default:
390 4
                    $this->dropDocumentCollection($class->name, $maxTimeMs, $writeConcern);
391
            }
392
        }
393 4
    }
394
395
    /**
396
     * Drop the document collection for a mapped class.
397
     *
398
     * @throws InvalidArgumentException
399
     */
400 18
    public function dropDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
401
    {
402 18
        $class = $this->dm->getClassMetadata($documentName);
403 18
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
404
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
405
        }
406
407 18
        $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
408
409 18
        $this->dm->getDocumentCollection($documentName)->drop($options);
410
411 18
        if (! $class->isFile) {
412 14
            return;
413
        }
414
415 8
        $this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop($options);
416 8
    }
417
418
    /**
419
     * Drop all the mapped document databases in the metadata factory.
420
     */
421 4
    public function dropDatabases(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
422
    {
423 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
424 4
            assert($class instanceof ClassMetadata);
425
            switch (true) {
426 4
                case $class->isMappedSuperclass:
427 4
                case $class->isEmbeddedDocument:
428 4
                case $class->isQueryResultDocument:
429 4
                    continue 2;
430
431 4
                case $class->isView():
432
                default:
433 4
                    $this->dropDocumentDatabase($class->name, $maxTimeMs, $writeConcern);
434
            }
435
        }
436 4
    }
437
438
    /**
439
     * Drop the document database for a mapped class.
440
     *
441
     * @throws InvalidArgumentException
442
     */
443 8
    public function dropDocumentDatabase(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
444
    {
445 8
        $class = $this->dm->getClassMetadata($documentName);
446 8
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
447
            throw new InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
448
        }
449
450 8
        $this->dm->getDocumentDatabase($documentName)->drop($this->getWriteOptions($maxTimeMs, $writeConcern));
451 8
    }
452
453 52
    public function isMongoIndexEquivalentToDocumentIndex(IndexInfo $mongoIndex, array $documentIndex) : bool
454
    {
455 52
        return $this->isEquivalentIndexKeys($mongoIndex, $documentIndex) && $this->isEquivalentIndexOptions($mongoIndex, $documentIndex);
456
    }
457
458
    /**
459
     * Determine if the keys for a MongoDB index can be considered equivalent to
460
     * those for an index in class metadata.
461
     */
462 52
    private function isEquivalentIndexKeys(IndexInfo $mongoIndex, array $documentIndex) : bool
463
    {
464 52
        $mongoIndexKeys    = $mongoIndex['key'];
465 52
        $documentIndexKeys = $documentIndex['keys'];
466
467
        /* If we are dealing with text indexes, we need to unset internal fields
468
         * from the MongoDB index and filter out text fields from the document
469
         * index. This will leave only non-text fields, which we can compare as
470
         * normal. Any text fields in the document index will be compared later
471
         * with isEquivalentTextIndexWeights(). */
472 52
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
473 15
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
474
475
            $documentIndexKeys = array_filter($documentIndexKeys, static function ($type) {
476 15
                return $type !== 'text';
477 15
            });
478
        }
479
480
        /* Avoid a strict equality check here. The numeric type returned by
481
         * MongoDB may differ from the document index without implying that the
482
         * indexes themselves are inequivalent. */
483
        // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
484 52
        return $mongoIndexKeys == $documentIndexKeys;
485
    }
486
487
    /**
488
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
489
     * considered equivalent to an index in class metadata based on options.
490
     *
491
     * Indexes are considered different if:
492
     *
493
     *   (a) Key/direction pairs differ or are not in the same order
494
     *   (b) Sparse or unique options differ
495
     *   (c) Geospatial options differ (bits, max, min)
496
     *   (d) The partialFilterExpression differs
497
     *
498
     * The background option is only relevant to index creation and is not
499
     * considered.
500
     */
501 45
    private function isEquivalentIndexOptions(IndexInfo $mongoIndex, array $documentIndex) : bool
502
    {
503 45
        $mongoIndexOptions = $mongoIndex->__debugInfo();
504 45
        unset($mongoIndexOptions['v'], $mongoIndexOptions['ns'], $mongoIndexOptions['key']);
505
506 45
        $documentIndexOptions = $documentIndex['options'];
507
508 45
        if ($this->indexOptionsAreMissing($mongoIndexOptions, $documentIndexOptions)) {
509 7
            return false;
510
        }
511
512 38
        if (empty($mongoIndexOptions['sparse']) xor empty($documentIndexOptions['sparse'])) {
513 2
            return false;
514
        }
515
516 36
        if (empty($mongoIndexOptions['unique']) xor empty($documentIndexOptions['unique'])) {
517 2
            return false;
518
        }
519
520 34
        foreach (['bits', 'max', 'min'] as $option) {
521 34
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
522 34
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
523 3
                return false;
524
            }
525
        }
526
527 31
        if (empty($mongoIndexOptions['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
528 2
            return false;
529
        }
530
531 29
        if (isset($mongoIndexOptions['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
532 29
            $mongoIndexOptions['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
533 1
            return false;
534
        }
535
536 28
        if (isset($mongoIndexOptions['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
537 2
            return false;
538
        }
539
540 26
        foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
541
            /* Text indexes will always report defaults for these options, so
542
             * only compare if we have explicit values in the document index. */
543 26
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
544 26
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
545 3
                return false;
546
            }
547
        }
548
549 23
        return true;
550
    }
551
552
    /**
553
     * Checks if any index options are missing.
554
     *
555
     * Options added to the ALLOWED_MISSING_INDEX_OPTIONS constant are ignored
556
     * and are expected to be checked later
557
     */
558 45
    private function indexOptionsAreMissing(array $mongoIndexOptions, array $documentIndexOptions) : bool
559
    {
560 45
        foreach (self::ALLOWED_MISSING_INDEX_OPTIONS as $option) {
561 45
            unset($mongoIndexOptions[$option], $documentIndexOptions[$option]);
562
        }
563
564 45
        return array_diff_key($mongoIndexOptions, $documentIndexOptions) !== [] || array_diff_key($documentIndexOptions, $mongoIndexOptions) !== [];
565
    }
566
567
    /**
568
     * Determine if the text index weights for a MongoDB index can be considered
569
     * equivalent to those for an index in class metadata.
570
     */
571 14
    private function isEquivalentTextIndexWeights(IndexInfo $mongoIndex, array $documentIndex) : bool
572
    {
573 14
        $mongoIndexWeights    = $mongoIndex['weights'];
574 14
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
575
576
        // If not specified, assign a default weight for text fields
577 14
        foreach ($documentIndex['keys'] as $key => $type) {
578 14
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
579 5
                continue;
580
            }
581
582 9
            $documentIndexWeights[$key] = 1;
583
        }
584
585
        /* MongoDB returns the weights sorted by field name, but we'll sort both
586
         * arrays in case that is internal behavior not to be relied upon. */
587 14
        ksort($mongoIndexWeights);
588 14
        ksort($documentIndexWeights);
589
590
        /* Avoid a strict equality check here. The numeric type returned by
591
         * MongoDB may differ from the document index without implying that the
592
         * indexes themselves are inequivalent. */
593
        // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
594 14
        return $mongoIndexWeights == $documentIndexWeights;
595
    }
596
597
    /**
598
     * Ensure collections are sharded for all documents that can be loaded with the
599
     * metadata factory.
600
     *
601
     * @throws MongoDBException
602
     */
603
    public function ensureSharding(?WriteConcern $writeConcern = null) : void
604
    {
605
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
606
            assert($class instanceof ClassMetadata);
607
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
608
                continue;
609
            }
610
611
            $this->ensureDocumentSharding($class->name, $writeConcern);
612
        }
613
    }
614
615
    /**
616
     * Ensure sharding for collection by document name.
617
     *
618
     * @throws MongoDBException
619
     */
620 12
    public function ensureDocumentSharding(string $documentName, ?WriteConcern $writeConcern = null) : void
621
    {
622 12
        $class = $this->dm->getClassMetadata($documentName);
623 12
        if (! $class->isSharded()) {
624
            return;
625
        }
626
627 12
        if ($this->collectionIsSharded($documentName)) {
628 1
            return;
629
        }
630
631 12
        $this->enableShardingForDbByDocumentName($documentName);
632
633
        try {
634 12
            $this->runShardCollectionCommand($documentName, $writeConcern);
635 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...
636 1
            throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage());
637
        }
638 11
    }
639
640
    /**
641
     * Enable sharding for database which contains documents with given name.
642
     *
643
     * @throws MongoDBException
644
     */
645 12
    public function enableShardingForDbByDocumentName(string $documentName) : void
646
    {
647 12
        $dbName  = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
648 12
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
649
650
        try {
651 12
            $adminDb->command(['enableSharding' => $dbName]);
652
        } 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...
653
            // Don't throw an exception if sharding is already enabled; there's just no other way to check this
654
            if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) {
655
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
656
            }
657
        } 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...
658
            throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
659
        }
660 12
    }
661
662 12
    private function runShardCollectionCommand(string $documentName, ?WriteConcern $writeConcern = null) : array
663
    {
664 12
        $class    = $this->dm->getClassMetadata($documentName);
665 12
        $dbName   = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
666 12
        $shardKey = $class->getShardKey();
667 12
        $adminDb  = $this->dm->getClient()->selectDatabase('admin');
668
669 12
        $shardKeyPart = [];
670 12
        foreach ($shardKey['keys'] as $key => $order) {
671 12
            if ($class->hasField($key)) {
672 1
                $mapping   = $class->getFieldMapping($key);
673 1
                $fieldName = $mapping['name'];
674
675 1
                if ($class->isSingleValuedReference($key)) {
676 1
                    $fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $fieldName);
677
                }
678
            } else {
679 11
                $fieldName = $key;
680
            }
681
682 12
            $shardKeyPart[$fieldName] = $order;
683
        }
684
685 12
        return $adminDb->command(
686 12
            array_merge(
687
                [
688 12
                    'shardCollection' => $dbName . '.' . $class->getCollection(),
689 12
                    'key'             => $shardKeyPart,
690
                ],
691 12
                $this->getWriteOptions(null, $writeConcern)
692
            )
693 11
        )->toArray()[0];
694
    }
695
696 10
    private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
697
    {
698 10
        $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern, $background);
699 10
        $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern, $background);
700 10
    }
701
702 10
    private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
703
    {
704 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...
705 10
        foreach ($chunksCollection->listIndexes() as $index) {
706
            if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) {
707
                return;
708
            }
709
        }
710
711 10
        $chunksCollection->createIndex(
712 10
            self::GRIDFS_FILE_COLLECTION_INDEX,
713 10
            $this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true, 'background' => $background])
714
        );
715 10
    }
716
717 10
    private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
718
    {
719 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...
720 10
        foreach ($filesCollection->listIndexes() as $index) {
721
            if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) {
722
                return;
723
            }
724
        }
725
726 10
        $filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern, ['background' => $background]));
727 10
    }
728
729 12
    private function collectionIsSharded(string $documentName) : bool
730
    {
731 12
        $class = $this->dm->getClassMetadata($documentName);
732
733 12
        $database    = $this->dm->getDocumentDatabase($documentName);
734 12
        $collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]);
735 12
        if (! iterator_count($collections)) {
736 8
            return false;
737
        }
738
739 4
        $stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0];
740
741 4
        return (bool) ($stats['sharded'] ?? false);
742
    }
743
744 102
    private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []) : array
745
    {
746 102
        unset($options['maxTimeMs'], $options['writeConcern']);
747
748 102
        if ($maxTimeMs !== null) {
749 36
            $options['maxTimeMs'] = $maxTimeMs;
750
        }
751
752 102
        if ($writeConcern !== null) {
753 36
            $options['writeConcern'] = $writeConcern;
754
        }
755
756 102
        return $options;
757
    }
758
}
759