Completed
Push — master ( 051ca4...be24ed )
by Andreas
19:03 queued 10s
created

SchemaManager::indexOptionsAreMissing()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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