Completed
Push — master ( ecb96f...9f19a4 )
by Andreas
21s queued 13s
created

SchemaManager::createDocumentCollection()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 6.0026

Importance

Changes 0
Metric Value
dl 0
loc 50
ccs 23
cts 24
cp 0.9583
rs 8.4686
c 0
b 0
f 0
cc 6
nc 4
nop 3
crap 6.0026
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB;
6
7
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
8
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory;
9
use Doctrine\ODM\MongoDB\Repository\ViewRepository;
10
use InvalidArgumentException;
11
use MongoDB\Driver\Exception\RuntimeException;
12
use MongoDB\Driver\Exception\ServerException;
13
use MongoDB\Driver\WriteConcern;
14
use MongoDB\Model\IndexInfo;
15
use function array_diff_key;
16
use function array_filter;
17
use function array_merge;
18
use function array_unique;
19
use function assert;
20
use function is_string;
21
use function iterator_count;
22
use function iterator_to_array;
23
use function ksort;
24
use function sprintf;
25
26
final class SchemaManager
27
{
28
    private const GRIDFS_FILE_COLLECTION_INDEX = ['files_id' => 1, 'n' => 1];
29
30
    private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1];
31
32
    private const CODE_SHARDING_ALREADY_INITIALIZED = 23;
33
34
    private const ALLOWED_MISSING_INDEX_OPTIONS = [
35
        'background',
36
        'partialFilterExpression',
37
        'sparse',
38
        'unique',
39
        'weights',
40
        'default_language',
41
        'language_override',
42
        'textIndexVersion',
43
        'name',
44
    ];
45
46
    /** @var DocumentManager */
47
    protected $dm;
48
49
    /** @var ClassMetadataFactory */
50 1800
    protected $metadataFactory;
51
52 1800
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
53 1800
    {
54 1800
        $this->dm              = $dm;
55
        $this->metadataFactory = $cmf;
56
    }
57
58
    /**
59
     * Ensure indexes are created for all documents that can be loaded with the
60 5
     * metadata factory.
61
     */
62 5
    public function ensureIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
63 5
    {
64 5
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
65 5
            assert($class instanceof ClassMetadata);
66
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
67
                continue;
68 5
            }
69
70 5
            $this->ensureDocumentIndexes($class->name, $maxTimeMs, $writeConcern, $background);
71
        }
72
    }
73
74
    /**
75
     * Ensure indexes exist for all mapped document classes.
76
     *
77
     * Indexes that exist in MongoDB but not the document metadata will be
78
     * deleted.
79
     */
80
    public function updateIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
81
    {
82
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
83
            assert($class instanceof ClassMetadata);
84
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
85
                continue;
86
            }
87
88
            $this->updateDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
89
        }
90
    }
91
92
    /**
93
     * Ensure indexes exist for the mapped document class.
94
     *
95
     * Indexes that exist in MongoDB but not the document metadata will be
96
     * deleted.
97
     *
98 9
     * @throws InvalidArgumentException
99
     */
100 9
    public function updateDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
101
    {
102 9
        $class = $this->dm->getClassMetadata($documentName);
103
104
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
105
            throw new InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.');
106 9
        }
107 9
108 9
        $documentIndexes = $this->getDocumentIndexes($documentName);
109
        $collection      = $this->dm->getDocumentCollection($documentName);
110
        $mongoIndexes    = iterator_to_array($collection->listIndexes());
111
112
        /* Determine which Mongo indexes should be deleted. Exclude the ID index
113 9
         * and those that are equivalent to any in the class metadata.
114
         */
115 4
        $self         = $this;
116
        $mongoIndexes = array_filter($mongoIndexes, static function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
117
            if ($mongoIndex['name'] === '_id_') {
118
                return false;
119 4
            }
120 4
121
            foreach ($documentIndexes as $documentIndex) {
122
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
123
                    return false;
124
                }
125 4
            }
126 9
127
            return true;
128
        });
129 9
130 4
        // Delete indexes that do not exist in class metadata
131
        foreach ($mongoIndexes as $mongoIndex) {
132
            if (! isset($mongoIndex['name'])) {
133
                continue;
134 4
            }
135
136
            $collection->dropIndex($mongoIndex['name'], $this->getWriteOptions($maxTimeMs, $writeConcern));
137 9
        }
138 9
139
        $this->ensureDocumentIndexes($documentName, $maxTimeMs, $writeConcern);
140 47
    }
141
142 47
    public function getDocumentIndexes(string $documentName) : array
143
    {
144 47
        $visited = [];
145
146
        return $this->doGetDocumentIndexes($documentName, $visited);
147 47
    }
148
149 47
    private function doGetDocumentIndexes(string $documentName, array &$visited) : array
150 5
    {
151
        if (isset($visited[$documentName])) {
152
            return [];
153 47
        }
154
155 47
        $visited[$documentName] = true;
156 47
157 47
        $class                   = $this->dm->getClassMetadata($documentName);
158
        $indexes                 = $this->prepareIndexes($class);
159
        $embeddedDocumentIndexes = [];
160 47
161 47
        // Add indexes from embedded & referenced documents
162 14
        foreach ($class->fieldMappings as $fieldMapping) {
163 14
            if (isset($fieldMapping['embedded'])) {
164 6
                if (isset($fieldMapping['targetDocument'])) {
165 1
                    $possibleEmbeds = [$fieldMapping['targetDocument']];
166
                } elseif (isset($fieldMapping['discriminatorMap'])) {
167 5
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
168
                } else {
169
                    continue;
170 14
                }
171 14
172 7
                foreach ($possibleEmbeds as $embed) {
173
                    if (isset($embeddedDocumentIndexes[$embed])) {
174 14
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
175 14
                    } else {
176
                        $embeddedIndexes                 = $this->doGetDocumentIndexes($embed, $visited);
177
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
178 14
                    }
179 4
180 4
                    foreach ($embeddedIndexes as $embeddedIndex) {
181 4
                        foreach ($embeddedIndex['keys'] as $key => $value) {
182
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
183
                            unset($embeddedIndex['keys'][$key]);
184 4
                        }
185 2
186
                        if (isset($embeddedIndex['options']['name'])) {
187
                            $embeddedIndex['options']['name'] = sprintf('%s_%s', $fieldMapping['name'], $embeddedIndex['options']['name']);
188 4
                        }
189
190
                        $indexes[] = $embeddedIndex;
191 47
                    }
192 29
                }
193 23
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
194 23
                foreach ($indexes as $idx => $index) {
195 23
                    $newKeys = [];
196 6
                    foreach ($index['keys'] as $key => $v) {
197
                        if ($key === $fieldMapping['name']) {
198
                            $key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key);
199 23
                        }
200
201
                        $newKeys[$key] = $v;
202 23
                    }
203
204
                    $indexes[$idx]['keys'] = $newKeys;
205
                }
206
            }
207 47
        }
208
209
        return $indexes;
210 47
    }
211
212 47
    private function prepareIndexes(ClassMetadata $class) : array
213 47
    {
214 47
        $persister  = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
215
        $indexes    = $class->getIndexes();
216 47
        $newIndexes = [];
217
218 41
        foreach ($indexes as $index) {
219 41
            $newIndex = [
220
                'keys' => [],
221
                'options' => $index['options'],
222 41
            ];
223 41
224 41
            foreach ($index['keys'] as $key => $value) {
225 36
                $key = $persister->prepareFieldName($key);
226 36
                if ($class->hasField($key)) {
227
                    $mapping                            = $class->getFieldMapping($key);
228 12
                    $newIndex['keys'][$mapping['name']] = $value;
229
                } else {
230
                    $newIndex['keys'][$key] = $value;
231
                }
232 41
            }
233
234
            $newIndexes[] = $newIndex;
235 47
        }
236
237
        return $newIndexes;
238
    }
239
240
    /**
241
     * Ensure the given document's indexes are created.
242
     *
243 43
     * @throws InvalidArgumentException
244
     */
245 43
    public function ensureDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
246 43
    {
247
        $class = $this->dm->getClassMetadata($documentName);
248
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
249
            throw new InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
250 43
        }
251 10
252
        if ($class->isFile) {
253
            $this->ensureGridFSIndexes($class, $maxTimeMs, $writeConcern, $background);
254 43
        }
255 43
256 13
        $indexes = $this->getDocumentIndexes($documentName);
257
        if (! $indexes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $indexes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
258
            return;
259 37
        }
260 37
261 37
        $collection = $this->dm->getDocumentCollection($class->name);
262
        foreach ($indexes as $index) {
263 36
            $collection->createIndex($index['keys'], $this->getWriteOptions($maxTimeMs, $writeConcern, $index['options'] + ['background' => $background]));
264
        }
265
    }
266
267
    /**
268
     * Delete indexes for all documents that can be loaded with the
269 4
     * metadata factory.
270
     */
271 4
    public function deleteIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
272 4
    {
273 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
274 4
            assert($class instanceof ClassMetadata);
275
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
276
                continue;
277 4
            }
278
279 4
            $this->deleteDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
280
        }
281
    }
282
283
    /**
284
     * Delete the given document's indexes.
285
     *
286 8
     * @throws InvalidArgumentException
287
     */
288 8
    public function deleteDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
289 8
    {
290
        $class = $this->dm->getClassMetadata($documentName);
291
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) {
292
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
293 8
        }
294 8
295
        $this->dm->getDocumentCollection($documentName)->dropIndexes($this->getWriteOptions($maxTimeMs, $writeConcern));
296
    }
297
298
    /**
299 4
     * Create all the mapped document collections in the metadata factory.
300
     */
301 4
    public function createCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
302 4
    {
303 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
304 4
            assert($class instanceof ClassMetadata);
305
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
306 4
                continue;
307
            }
308 4
            $this->createDocumentCollection($class->name, $maxTimeMs, $writeConcern);
309
        }
310
    }
311
312
    /**
313
     * Create the document collection for a mapped class.
314
     *
315 14
     * @throws InvalidArgumentException
316
     */
317 14
    public function createDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
318
    {
319 14
        $class = $this->dm->getClassMetadata($documentName);
320
321
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
322
            throw new InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
323 14
        }
324 8
325
        if ($class->isView()) {
326 8
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
327 8
328
            $rootClass = $class->getRootClass();
329 8
            assert(is_string($rootClass));
330
331
            $builder    = $this->dm->createAggregationBuilder($rootClass);
332
            $repository = $this->dm->getRepository($class->name);
333 10
            assert($repository instanceof ViewRepository);
334 10
            $repository->createViewAggregation($builder);
335 10
336
            $collectionName = $this->dm->getDocumentCollection($rootClass)->getCollectionName();
337
            $this->dm->getDocumentDatabase($documentName)
338 10
                ->command([
339 10
                    'create' => $class->collection,
340 10
                    'viewOn' => $collectionName,
341
                    'pipeline' => $builder->getPipeline(),
342 10
                ], $options);
343
344
            return;
345
        }
346
347 4
        if ($class->isFile) {
348
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
349 4
350 4
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files', $options);
351 4
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks', $options);
352 4
353
            return;
354
        }
355 4
356
        $options = [
357 4
            'capped' => $class->getCollectionCapped(),
358
            'size' => $class->getCollectionSize(),
359
            'max' => $class->getCollectionMax(),
360
        ];
361
362
        $this->dm->getDocumentDatabase($documentName)->createCollection(
363
            $class->getCollection(),
364 14
            $this->getWriteOptions($maxTimeMs, $writeConcern, $options)
365
        );
366 14
    }
367 14
368
    /**
369
     * Drop all the mapped document collections in the metadata factory.
370
     */
371 14
    public function dropCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
372
    {
373 14
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
374
            assert($class instanceof ClassMetadata);
375 14
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
376 10
                continue;
377
            }
378
379 8
            $this->dropDocumentCollection($class->name, $maxTimeMs, $writeConcern);
380 8
        }
381
    }
382
383
    /**
384
     * Drop the document collection for a mapped class.
385 4
     *
386
     * @throws InvalidArgumentException
387 4
     */
388 4
    public function dropDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
389 4
    {
390 4
        $class = $this->dm->getClassMetadata($documentName);
391
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
392
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
393 4
        }
394
395 4
        $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
396
397
        $this->dm->getDocumentCollection($documentName)->drop($options);
398
399
        if (! $class->isFile) {
400
            return;
401
        }
402 8
403
        $this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop($options);
404 8
    }
405 8
406
    /**
407
     * Drop all the mapped document databases in the metadata factory.
408
     */
409 8
    public function dropDatabases(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
410 8
    {
411
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
412 52
            assert($class instanceof ClassMetadata);
413
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
414 52
                continue;
415
            }
416
417
            $this->dropDocumentDatabase($class->name, $maxTimeMs, $writeConcern);
418
        }
419
    }
420
421 52
    /**
422
     * Drop the document database for a mapped class.
423 52
     *
424 52
     * @throws InvalidArgumentException
425
     */
426
    public function dropDocumentDatabase(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
427
    {
428
        $class = $this->dm->getClassMetadata($documentName);
429
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
430
            throw new InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
431 52
        }
432 15
433
        $this->dm->getDocumentDatabase($documentName)->drop($this->getWriteOptions($maxTimeMs, $writeConcern));
434
    }
435 15
436 15
    public function isMongoIndexEquivalentToDocumentIndex(IndexInfo $mongoIndex, array $documentIndex) : bool
437
    {
438
        return $this->isEquivalentIndexKeys($mongoIndex, $documentIndex) && $this->isEquivalentIndexOptions($mongoIndex, $documentIndex);
439
    }
440
441
    /**
442
     * Determine if the keys for a MongoDB index can be considered equivalent to
443 52
     * those for an index in class metadata.
444
     */
445
    private function isEquivalentIndexKeys(IndexInfo $mongoIndex, array $documentIndex) : bool
446
    {
447
        $mongoIndexKeys    = $mongoIndex['key'];
448
        $documentIndexKeys = $documentIndex['keys'];
449
450
        /* If we are dealing with text indexes, we need to unset internal fields
451
         * from the MongoDB index and filter out text fields from the document
452
         * index. This will leave only non-text fields, which we can compare as
453
         * normal. Any text fields in the document index will be compared later
454
         * with isEquivalentTextIndexWeights(). */
455
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
456
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
457
458
            $documentIndexKeys = array_filter($documentIndexKeys, static function ($type) {
459
                return $type !== 'text';
460 45
            });
461
        }
462 45
463 45
        /* Avoid a strict equality check here. The numeric type returned by
464
         * MongoDB may differ from the document index without implying that the
465 45
         * indexes themselves are inequivalent. */
466
        // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
467 45
        return $mongoIndexKeys == $documentIndexKeys;
468 7
    }
469
470
    /**
471 38
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
472 2
     * considered equivalent to an index in class metadata based on options.
473
     *
474
     * Indexes are considered different if:
475 36
     *
476 2
     *   (a) Key/direction pairs differ or are not in the same order
477
     *   (b) Sparse or unique options differ
478
     *   (c) Geospatial options differ (bits, max, min)
479 34
     *   (d) The partialFilterExpression differs
480 34
     *
481 34
     * The background option is only relevant to index creation and is not
482 3
     * considered.
483
     */
484
    private function isEquivalentIndexOptions(IndexInfo $mongoIndex, array $documentIndex) : bool
485
    {
486 31
        $mongoIndexOptions = $mongoIndex->__debugInfo();
487 2
        unset($mongoIndexOptions['v'], $mongoIndexOptions['ns'], $mongoIndexOptions['key']);
488
489
        $documentIndexOptions = $documentIndex['options'];
490 29
491 29
        if ($this->indexOptionsAreMissing($mongoIndexOptions, $documentIndexOptions)) {
492 1
            return false;
493
        }
494
495 28
        if (empty($mongoIndexOptions['sparse']) xor empty($documentIndexOptions['sparse'])) {
496 2
            return false;
497
        }
498
499 26
        if (empty($mongoIndexOptions['unique']) xor empty($documentIndexOptions['unique'])) {
500
            return false;
501
        }
502 26
503 26
        foreach (['bits', 'max', 'min'] as $option) {
504 3
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
505
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
506
                return false;
507
            }
508 23
        }
509
510
        if (empty($mongoIndexOptions['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
511
            return false;
512
        }
513
514
        if (isset($mongoIndexOptions['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
515
            $mongoIndexOptions['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
516
            return false;
517 45
        }
518
519 45
        if (isset($mongoIndexOptions['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
520 45
            return false;
521
        }
522
523 45
        foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
524
            /* Text indexes will always report defaults for these options, so
525
             * only compare if we have explicit values in the document index. */
526
            if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) &&
527
                $mongoIndexOptions[$option] !== $documentIndexOptions[$option]) {
528
                return false;
529
            }
530 14
        }
531
532 14
        return true;
533 14
    }
534
535
    /**
536 14
     * Checks if any index options are missing.
537 14
     *
538 5
     * Options added to the ALLOWED_MISSING_INDEX_OPTIONS constant are ignored
539
     * and are expected to be checked later
540
     */
541 9
    private function indexOptionsAreMissing(array $mongoIndexOptions, array $documentIndexOptions) : bool
542
    {
543
        foreach (self::ALLOWED_MISSING_INDEX_OPTIONS as $option) {
544
            unset($mongoIndexOptions[$option], $documentIndexOptions[$option]);
545
        }
546 14
547 14
        return array_diff_key($mongoIndexOptions, $documentIndexOptions) !== [] || array_diff_key($documentIndexOptions, $mongoIndexOptions) !== [];
548
    }
549
550
    /**
551
     * Determine if the text index weights for a MongoDB index can be considered
552
     * equivalent to those for an index in class metadata.
553 14
     */
554
    private function isEquivalentTextIndexWeights(IndexInfo $mongoIndex, array $documentIndex) : bool
555
    {
556
        $mongoIndexWeights    = $mongoIndex['weights'];
557
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
558
559
        // If not specified, assign a default weight for text fields
560
        foreach ($documentIndex['keys'] as $key => $type) {
561
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
562
                continue;
563
            }
564
565
            $documentIndexWeights[$key] = 1;
566
        }
567
568
        /* MongoDB returns the weights sorted by field name, but we'll sort both
569
         * arrays in case that is internal behavior not to be relied upon. */
570
        ksort($mongoIndexWeights);
571
        ksort($documentIndexWeights);
572
573
        /* Avoid a strict equality check here. The numeric type returned by
574
         * MongoDB may differ from the document index without implying that the
575
         * indexes themselves are inequivalent. */
576
        // phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator
577
        return $mongoIndexWeights == $documentIndexWeights;
578
    }
579 12
580
    /**
581 12
     * Ensure collections are sharded for all documents that can be loaded with the
582 12
     * metadata factory.
583
     *
584
     * @throws MongoDBException
585
     */
586 12
    public function ensureSharding(?WriteConcern $writeConcern = null) : void
587 1
    {
588
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
589
            assert($class instanceof ClassMetadata);
590 12
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
591
                continue;
592
            }
593 12
594 1
            $this->ensureDocumentSharding($class->name, $writeConcern);
595 1
        }
596
    }
597 11
598
    /**
599
     * Ensure sharding for collection by document name.
600
     *
601
     * @throws MongoDBException
602
     */
603
    public function ensureDocumentSharding(string $documentName, ?WriteConcern $writeConcern = null) : void
604 12
    {
605
        $class = $this->dm->getClassMetadata($documentName);
606 12
        if (! $class->isSharded()) {
607 12
            return;
608
        }
609
610 12
        if ($this->collectionIsSharded($documentName)) {
611
            return;
612
        }
613
614
        $this->enableShardingForDbByDocumentName($documentName);
615
616
        try {
617
            $this->runShardCollectionCommand($documentName, $writeConcern);
618
        } catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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

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

Loading history...
619 12
            throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage());
620
        }
621 12
    }
622
623 12
    /**
624 12
     * Enable sharding for database which contains documents with given name.
625 12
     *
626 12
     * @throws MongoDBException
627
     */
628 12
    public function enableShardingForDbByDocumentName(string $documentName) : void
629 12
    {
630 12
        $dbName  = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
631 1
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
632 1
633
        try {
634 1
            $adminDb->command(['enableSharding' => $dbName]);
635 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...
636
            // Don't throw an exception if sharding is already enabled; there's just no other way to check this
637
            if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) {
638 11
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
639
            }
640
        } catch (RuntimeException $e) {
0 ignored issues
show
Bug introduced by
The class MongoDB\Driver\Exception\RuntimeException does not exist. Did you forget a USE statement, or did you not list all dependencies?

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

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

Loading history...
641 12
            throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
642
        }
643
    }
644 12
645 12
    private function runShardCollectionCommand(string $documentName, ?WriteConcern $writeConcern = null) : array
646
    {
647 12
        $class    = $this->dm->getClassMetadata($documentName);
648 12
        $dbName   = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
649
        $shardKey = $class->getShardKey();
650 12
        $adminDb  = $this->dm->getClient()->selectDatabase('admin');
651
652 11
        $shardKeyPart = [];
653
        foreach ($shardKey['keys'] as $key => $order) {
654
            if ($class->hasField($key)) {
655 10
                $mapping   = $class->getFieldMapping($key);
656
                $fieldName = $mapping['name'];
657 10
658 10
                if ($class->isSingleValuedReference($key)) {
659 10
                    $fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $fieldName);
660
                }
661 10
            } else {
662
                $fieldName = $key;
663 10
            }
664 10
665
            $shardKeyPart[$fieldName] = $order;
666
        }
667
668
        return $adminDb->command(
669
            array_merge(
670 10
                [
671 10
                    'shardCollection' => $dbName . '.' . $class->getCollection(),
672 10
                    'key'             => $shardKeyPart,
673
                ],
674 10
                $this->getWriteOptions(null, $writeConcern)
675
            )
676 10
        )->toArray()[0];
677
    }
678 10
679 10
    private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
680
    {
681
        $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern, $background);
682
        $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern, $background);
683
    }
684
685 10
    private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
686 10
    {
687
        $chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection();
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
688 12
        foreach ($chunksCollection->listIndexes() as $index) {
689
            if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) {
690 12
                return;
691
            }
692 12
        }
693 12
694 12
        $chunksCollection->createIndex(
695 8
            self::GRIDFS_FILE_COLLECTION_INDEX,
696
            $this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true, 'background' => $background])
697
        );
698 4
    }
699
700 4
    private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void
701
    {
702
        $filesCollection = $this->dm->getDocumentCollection($class->getName());
0 ignored issues
show
Bug introduced by
Consider using $class->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
703 93
        foreach ($filesCollection->listIndexes() as $index) {
704
            if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) {
705 93
                return;
706
            }
707 93
        }
708 32
709
        $filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern, ['background' => $background]));
710
    }
711 93
712 32
    private function collectionIsSharded(string $documentName) : bool
713
    {
714
        $class = $this->dm->getClassMetadata($documentName);
715 93
716
        $database    = $this->dm->getDocumentDatabase($documentName);
717
        $collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]);
718
        if (! iterator_count($collections)) {
719
            return false;
720
        }
721
722
        $stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0];
723
724
        return (bool) ($stats['sharded'] ?? false);
725
    }
726
727
    private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []) : array
728
    {
729
        unset($options['maxTimeMs'], $options['writeConcern']);
730
731
        if ($maxTimeMs !== null) {
732
            $options['maxTimeMs'] = $maxTimeMs;
733
        }
734
735
        if ($writeConcern !== null) {
736
            $options['writeConcern'] = $writeConcern;
737
        }
738
739
        return $options;
740
    }
741
}
742