SchemaManager   F
last analyzed

Complexity

Total Complexity 138

Size/Duplication

Total Lines 667
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 88.93%

Importance

Changes 0
Metric Value
wmc 138
lcom 1
cbo 4
dl 0
loc 667
ccs 265
cts 298
cp 0.8893
rs 1.933
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getDocumentIndexes() 0 5 1
C doGetDocumentIndexes() 0 62 16
A enableShardingForDbByDocumentName() 0 16 4
A __construct() 0 5 1
A ensureIndexes() 0 11 5
A updateIndexes() 0 11 5
B updateDocumentIndexes() 0 41 9
A prepareIndexes() 0 27 4
B ensureDocumentIndexes() 0 21 7
A deleteIndexes() 0 11 5
A deleteDocumentIndexes() 0 9 4
A createCollections() 0 10 5
A createDocumentCollection() 0 28 5
A dropCollections() 0 11 5
A dropDocumentCollection() 0 17 5
A dropDatabases() 0 11 5
A dropDocumentDatabase() 0 9 4
C isMongoIndexEquivalentToDocumentIndex() 0 51 16
A isEquivalentIndexKeys() 0 24 3
A isEquivalentTextIndexWeights() 0 25 4
A ensureSharding() 0 11 4
A ensureDocumentSharding() 0 19 4
A runShardCollectionCommand() 0 33 4
A ensureGridFSIndexes() 0 5 1
A ensureChunksIndex() 0 14 4
A ensureFilesIndex() 0 11 3
A collectionIsSharded() 0 13 2
A getWriteOptions() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like SchemaManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaManager, and based on these observations, apply Extract Interface, too.

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