Completed
Pull Request — master (#1911)
by Andreas
23:55
created

SchemaManager::ensureIndexes()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
592 12
            throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage());
593
        }
594 12
    }
595
596 12
    /**
597 12
     * Enable sharding for database which contains documents with given name.
598 12
     *
599 12
     * @throws MongoDBException
600
     */
601 12
    public function enableShardingForDbByDocumentName(string $documentName) : void
602 12
    {
603 12
        $dbName  = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
604 1
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
605 1
606
        try {
607 1
            $adminDb->command(['enableSharding' => $dbName]);
608 1
        } 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...
609
            // Don't throw an exception if sharding is already enabled; there's just no other way to check this
610
            if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) {
611 11
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
612
            }
613
        } 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...
614 12
            throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
615
        }
616
    }
617 12
618 12
    private function runShardCollectionCommand(string $documentName, ?WriteConcern $writeConcern = null) : array
619
    {
620 12
        $class    = $this->dm->getClassMetadata($documentName);
621 12
        $dbName   = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
622
        $shardKey = $class->getShardKey();
623 12
        $adminDb  = $this->dm->getClient()->selectDatabase('admin');
624
625 11
        $shardKeyPart = [];
626
        foreach ($shardKey['keys'] as $key => $order) {
627
            if ($class->hasField($key)) {
628 8
                $mapping   = $class->getFieldMapping($key);
629
                $fieldName = $mapping['name'];
630 8
631 8
                if ($class->isSingleValuedReference($key)) {
632 8
                    $fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $fieldName);
633
                }
634 8
            } else {
635
                $fieldName = $key;
636 8
            }
637 8
638
            $shardKeyPart[$fieldName] = $order;
639
        }
640
641
        return $adminDb->command(
642
            array_merge(
643 8
                [
644 8
                    'shardCollection' => $dbName . '.' . $class->getCollection(),
645 8
                    'key'             => $shardKeyPart,
646
                ],
647 8
                $this->getWriteOptions(null, $writeConcern)
648
            )
649 8
        )->toArray()[0];
650
    }
651 8
652 8
    private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
653
    {
654
        $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern, $background);
655
        $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern, $background);
656
    }
657
658 8
    private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
659 8
    {
660
        $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...
661 12
        foreach ($chunksCollection->listIndexes() as $index) {
662
            if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) {
663 12
                return;
664
            }
665 12
        }
666 12
667 12
        $chunksCollection->createIndex(
668 8
            self::GRIDFS_FILE_COLLECTION_INDEX,
669
            $this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true, 'background' => $background])
670
        );
671 4
    }
672 4
673
    private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
674
    {
675 89
        $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...
676
        foreach ($filesCollection->listIndexes() as $index) {
677 89
            if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) {
678
                return;
679 89
            }
680 32
        }
681
682
        $filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern, ['background' => $background]));
683 89
    }
684 32
685
    private function collectionIsSharded(string $documentName) : bool
686
    {
687 89
        $class = $this->dm->getClassMetadata($documentName);
688
689
        $database    = $this->dm->getDocumentDatabase($documentName);
690
        $collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]);
691
        if (! iterator_count($collections)) {
692
            return false;
693
        }
694
695
        $stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0];
696
        return (bool) ($stats['sharded'] ?? false);
697
    }
698
699
    private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []) : array
700
    {
701
        unset($options['maxTimeMs'], $options['writeConcern']);
702
703
        if ($maxTimeMs !== null) {
704
            $options['maxTimeMs'] = $maxTimeMs;
705
        }
706
707
        if ($writeConcern !== null) {
708
            $options['writeConcern'] = $writeConcern;
709
        }
710
711
        return $options;
712
    }
713
}
714