Completed
Push — master ( 7b9f4b...1cd743 )
by Andreas
13s queued 10s
created

lib/Doctrine/ODM/MongoDB/SchemaManager.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
22
class SchemaManager
23
{
24
    private const GRIDFS_FILE_COLLECTION_INDEX = ['files_id' => 1, 'n' => 1];
25
26
    private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1];
27
28
    private const CODE_SHARDING_ALREADY_INITIALIZED = 23;
29
30
    /** @var DocumentManager */
31
    protected $dm;
32
33
    /** @var ClassMetadataFactory */
34
    protected $metadataFactory;
35
36 1757
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
37
    {
38 1757
        $this->dm              = $dm;
39 1757
        $this->metadataFactory = $cmf;
40 1757
    }
41
42
    /**
43
     * Ensure indexes are created for all documents that can be loaded with the
44
     * metadata factory.
45
     */
46 4
    public function ensureIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
47
    {
48 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
49 4
            assert($class instanceof ClassMetadata);
50 4
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
51 4
                continue;
52
            }
53
54 4
            $this->ensureDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
55
        }
56 4
    }
57
58
    /**
59
     * Ensure indexes exist for all mapped document classes.
60
     *
61
     * Indexes that exist in MongoDB but not the document metadata will be
62
     * deleted.
63
     */
64
    public function updateIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
65
    {
66
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
67
            assert($class instanceof ClassMetadata);
68
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
69
                continue;
70
            }
71
72
            $this->updateDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
73
        }
74
    }
75
76
    /**
77
     * Ensure indexes exist for the mapped document class.
78
     *
79
     * Indexes that exist in MongoDB but not the document metadata will be
80
     * deleted.
81
     *
82
     * @throws InvalidArgumentException
83
     */
84 9
    public function updateDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
85
    {
86 9
        $class = $this->dm->getClassMetadata($documentName);
87
88 9
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
89
            throw new InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.');
90
        }
91
92 9
        $documentIndexes = $this->getDocumentIndexes($documentName);
93 9
        $collection      = $this->dm->getDocumentCollection($documentName);
94 9
        $mongoIndexes    = iterator_to_array($collection->listIndexes());
95
96
        /* Determine which Mongo indexes should be deleted. Exclude the ID index
97
         * and those that are equivalent to any in the class metadata.
98
         */
99 9
        $self         = $this;
100
        $mongoIndexes = array_filter($mongoIndexes, static function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
101 4
            if ($mongoIndex['name'] === '_id_') {
102
                return false;
103
            }
104
105 4
            foreach ($documentIndexes as $documentIndex) {
106 4
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
107
                    return false;
108
                }
109
            }
110
111 4
            return true;
112 9
        });
113
114
        // Delete indexes that do not exist in class metadata
115 9
        foreach ($mongoIndexes as $mongoIndex) {
116 4
            if (! isset($mongoIndex['name'])) {
117
                continue;
118
            }
119
120 4
            $collection->dropIndex($mongoIndex['name'], $this->getWriteOptions($maxTimeMs, $writeConcern));
121
        }
122
123 9
        $this->ensureDocumentIndexes($documentName, $maxTimeMs, $writeConcern);
124 9
    }
125
126 40
    public function getDocumentIndexes(string $documentName) : array
127
    {
128 40
        $visited = [];
129 40
        return $this->doGetDocumentIndexes($documentName, $visited);
130
    }
131
132 40
    private function doGetDocumentIndexes(string $documentName, array &$visited) : array
133
    {
134 40
        if (isset($visited[$documentName])) {
135 4
            return [];
136
        }
137
138 40
        $visited[$documentName] = true;
139
140 40
        $class                   = $this->dm->getClassMetadata($documentName);
141 40
        $indexes                 = $this->prepareIndexes($class);
142 40
        $embeddedDocumentIndexes = [];
143
144
        // Add indexes from embedded & referenced documents
145 40
        foreach ($class->fieldMappings as $fieldMapping) {
146 40
            if (isset($fieldMapping['embedded'])) {
147 10
                if (isset($fieldMapping['targetDocument'])) {
148 10
                    $possibleEmbeds = [$fieldMapping['targetDocument']];
149 5
                } elseif (isset($fieldMapping['discriminatorMap'])) {
150 1
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
151
                } else {
152 4
                    continue;
153
                }
154
155 10
                foreach ($possibleEmbeds as $embed) {
156 10
                    if (isset($embeddedDocumentIndexes[$embed])) {
157 5
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
158
                    } else {
159 10
                        $embeddedIndexes                 = $this->doGetDocumentIndexes($embed, $visited);
160 10
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
161
                    }
162
163 10
                    foreach ($embeddedIndexes as $embeddedIndex) {
164 2
                        foreach ($embeddedIndex['keys'] as $key => $value) {
165 2
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
166 2
                            unset($embeddedIndex['keys'][$key]);
167
                        }
168
169 2
                        $indexes[] = $embeddedIndex;
170
                    }
171
                }
172 40
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
173 25
                foreach ($indexes as $idx => $index) {
174 21
                    $newKeys = [];
175 21
                    foreach ($index['keys'] as $key => $v) {
176 21
                        if ($key === $fieldMapping['name']) {
177 5
                            $key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key);
178
                        }
179
180 21
                        $newKeys[$key] = $v;
181
                    }
182
183 21
                    $indexes[$idx]['keys'] = $newKeys;
184
                }
185
            }
186
        }
187
188 40
        return $indexes;
189
    }
190
191 40
    private function prepareIndexes(ClassMetadata $class) : array
192
    {
193 40
        $persister  = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
194 40
        $indexes    = $class->getIndexes();
195 40
        $newIndexes = [];
196
197 40
        foreach ($indexes as $index) {
198
            $newIndex = [
199 36
                'keys' => [],
200 36
                'options' => $index['options'],
201
            ];
202
203 36
            foreach ($index['keys'] as $key => $value) {
204 36
                $key = $persister->prepareFieldName($key);
205 36
                if ($class->hasField($key)) {
206 31
                    $mapping                            = $class->getFieldMapping($key);
207 31
                    $newIndex['keys'][$mapping['name']] = $value;
208
                } else {
209 11
                    $newIndex['keys'][$key] = $value;
210
                }
211
            }
212
213 36
            $newIndexes[] = $newIndex;
214
        }
215
216 40
        return $newIndexes;
217
    }
218
219
    /**
220
     * Ensure the given document's indexes are created.
221
     *
222
     * @throws InvalidArgumentException
223
     */
224 36
    public function ensureDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
225
    {
226 36
        $class = $this->dm->getClassMetadata($documentName);
227 36
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
228
            throw new InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
229
        }
230
231 36
        if ($class->isFile) {
232 8
            $this->ensureGridFSIndexes($class, $maxTimeMs, $writeConcern);
233
        }
234
235 36
        $indexes = $this->getDocumentIndexes($documentName);
236 36
        if (! $indexes) {
237 10
            return;
238
        }
239
240 32
        $collection = $this->dm->getDocumentCollection($class->name);
0 ignored issues
show
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
241 32
        foreach ($indexes as $index) {
242 32
            $collection->createIndex($index['keys'], $this->getWriteOptions($maxTimeMs, $writeConcern, $index['options']));
243
        }
244 32
    }
245
246
    /**
247
     * Delete indexes for all documents that can be loaded with the
248
     * metadata factory.
249
     */
250 4
    public function deleteIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
251
    {
252 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
253 4
            assert($class instanceof ClassMetadata);
254 4
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
255 4
                continue;
256
            }
257
258 4
            $this->deleteDocumentIndexes($class->name, $maxTimeMs, $writeConcern);
259
        }
260 4
    }
261
262
    /**
263
     * Delete the given document's indexes.
264
     *
265
     * @throws InvalidArgumentException
266
     */
267 8
    public function deleteDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
268
    {
269 8
        $class = $this->dm->getClassMetadata($documentName);
270 8
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
271
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
272
        }
273
274 8
        $this->dm->getDocumentCollection($documentName)->dropIndexes($this->getWriteOptions($maxTimeMs, $writeConcern));
275 8
    }
276
277
    /**
278
     * Create all the mapped document collections in the metadata factory.
279
     */
280 4
    public function createCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
281
    {
282 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
283 4
            assert($class instanceof ClassMetadata);
284 4
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
285 4
                continue;
286
            }
287 4
            $this->createDocumentCollection($class->name, $maxTimeMs, $writeConcern);
288
        }
289 4
    }
290
291
    /**
292
     * Create the document collection for a mapped class.
293
     *
294
     * @throws InvalidArgumentException
295
     */
296 14
    public function createDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
297
    {
298 14
        $class = $this->dm->getClassMetadata($documentName);
299
300 14
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
301
            throw new InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
302
        }
303
304 14
        if ($class->isFile) {
305 8
            $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
306
307 8
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files', $options);
308 8
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks', $options);
309
310 8
            return;
311
        }
312
313
        $options = [
314 10
            'capped' => $class->getCollectionCapped(),
315 10
            'size' => $class->getCollectionSize(),
316 10
            'max' => $class->getCollectionMax(),
317
        ];
318
319 10
        $this->dm->getDocumentDatabase($documentName)->createCollection(
320 10
            $class->getCollection(),
321 10
            $this->getWriteOptions($maxTimeMs, $writeConcern, $options)
322
        );
323 10
    }
324
325
    /**
326
     * Drop all the mapped document collections in the metadata factory.
327
     */
328 4
    public function dropCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
329
    {
330 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
331 4
            assert($class instanceof ClassMetadata);
332 4
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
333 4
                continue;
334
            }
335
336 4
            $this->dropDocumentCollection($class->name, $maxTimeMs, $writeConcern);
337
        }
338 4
    }
339
340
    /**
341
     * Drop the document collection for a mapped class.
342
     *
343
     * @throws InvalidArgumentException
344
     */
345 14
    public function dropDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
346
    {
347 14
        $class = $this->dm->getClassMetadata($documentName);
348 14
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
349
            throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
350
        }
351
352 14
        $options = $this->getWriteOptions($maxTimeMs, $writeConcern);
353
354 14
        $this->dm->getDocumentCollection($documentName)->drop($options);
355
356 14
        if (! $class->isFile) {
357 10
            return;
358
        }
359
360 8
        $this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop($options);
361 8
    }
362
363
    /**
364
     * Drop all the mapped document databases in the metadata factory.
365
     */
366 4
    public function dropDatabases(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
367
    {
368 4
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
369 4
            assert($class instanceof ClassMetadata);
370 4
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
371 4
                continue;
372
            }
373
374 4
            $this->dropDocumentDatabase($class->name, $maxTimeMs, $writeConcern);
375
        }
376 4
    }
377
378
    /**
379
     * Drop the document database for a mapped class.
380
     *
381
     * @throws InvalidArgumentException
382
     */
383 8
    public function dropDocumentDatabase(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
384
    {
385 8
        $class = $this->dm->getClassMetadata($documentName);
386 8
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
387
            throw new InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
388
        }
389
390 8
        $this->dm->getDocumentDatabase($documentName)->drop($this->getWriteOptions($maxTimeMs, $writeConcern));
391 8
    }
392
393
    /**
394
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
395
     * considered equivalent to an index in class metadata.
396
     *
397
     * Indexes are considered different if:
398
     *
399
     *   (a) Key/direction pairs differ or are not in the same order
400
     *   (b) Sparse or unique options differ
401
     *   (c) Geospatial options differ (bits, max, min)
402
     *   (d) The partialFilterExpression differs
403
     *
404
     * The background option is only relevant to index creation and is not
405
     * considered.
406
     *
407
     * @param array|IndexInfo $mongoIndex Mongo index data.
408
     */
409 46
    public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, array $documentIndex) : bool
410
    {
411 46
        $documentIndexOptions = $documentIndex['options'];
412
413 46
        if (! $this->isEquivalentIndexKeys($mongoIndex, $documentIndex)) {
414 7
            return false;
415
        }
416
417 39
        if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) {
418 2
            return false;
419
        }
420
421 37
        if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) {
422 2
            return false;
423
        }
424
425 35
        foreach (['bits', 'max', 'min'] as $option) {
426 35
            if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) {
427 6
                return false;
428
            }
429
430 33
            if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
431 33
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
432 3
                return false;
433
            }
434
        }
435
436 26
        if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
437 2
            return false;
438
        }
439
440 24
        if (isset($mongoIndex['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) &&
441 24
            $mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
442 1
            return false;
443
        }
444
445 23
        if (isset($mongoIndex['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) {
446 2
            return false;
447
        }
448
449 21
        foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) {
450
            /* Text indexes will always report defaults for these options, so
451
             * only compare if we have explicit values in the document index. */
452 21
            if (isset($mongoIndex[$option], $documentIndexOptions[$option]) &&
453 21
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
454 3
                return false;
455
            }
456
        }
457
458 18
        return true;
459
    }
460
461
    /**
462
     * Determine if the keys for a MongoDB index can be considered equivalent to
463
     * those for an index in class metadata.
464
     *
465
     * @param array|IndexInfo $mongoIndex Mongo index data.
466
     */
467 46
    private function isEquivalentIndexKeys($mongoIndex, array $documentIndex) : bool
468
    {
469 46
        $mongoIndexKeys    = $mongoIndex['key'];
470 46
        $documentIndexKeys = $documentIndex['keys'];
471
472
        /* If we are dealing with text indexes, we need to unset internal fields
473
         * from the MongoDB index and filter out text fields from the document
474
         * index. This will leave only non-text fields, which we can compare as
475
         * normal. Any text fields in the document index will be compared later
476
         * with isEquivalentTextIndexWeights(). */
477 46
        if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') {
478 15
            unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']);
479
480
            $documentIndexKeys = array_filter($documentIndexKeys, static function ($type) {
481 15
                return $type !== 'text';
482 15
            });
483
        }
484
485
        /* Avoid a strict equality check here. The numeric type returned by
486
         * MongoDB may differ from the document index without implying that the
487
         * indexes themselves are inequivalent. */
488
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
489 46
        return $mongoIndexKeys == $documentIndexKeys;
490
    }
491
492
    /**
493
     * Determine if the text index weights for a MongoDB index can be considered
494
     * equivalent to those for an index in class metadata.
495
     *
496
     * @param array|IndexInfo $mongoIndex Mongo index data.
497
     */
498 14
    private function isEquivalentTextIndexWeights($mongoIndex, array $documentIndex) : bool
499
    {
500 14
        $mongoIndexWeights    = $mongoIndex['weights'];
501 14
        $documentIndexWeights = $documentIndex['options']['weights'] ?? [];
502
503
        // If not specified, assign a default weight for text fields
504 14
        foreach ($documentIndex['keys'] as $key => $type) {
505 14
            if ($type !== 'text' || isset($documentIndexWeights[$key])) {
506 5
                continue;
507
            }
508
509 9
            $documentIndexWeights[$key] = 1;
510
        }
511
512
        /* MongoDB returns the weights sorted by field name, but we'll sort both
513
         * arrays in case that is internal behavior not to be relied upon. */
514 14
        ksort($mongoIndexWeights);
515 14
        ksort($documentIndexWeights);
516
517
        /* Avoid a strict equality check here. The numeric type returned by
518
         * MongoDB may differ from the document index without implying that the
519
         * indexes themselves are inequivalent. */
520
        // phpcs:disable SlevomatCodingStandard.ControlStructures.DisallowEqualOperators.DisallowedEqualOperator
521 14
        return $mongoIndexWeights == $documentIndexWeights;
522
    }
523
524
    /**
525
     * Ensure collections are sharded for all documents that can be loaded with the
526
     * metadata factory.
527
     *
528
     * @throws MongoDBException
529
     */
530
    public function ensureSharding(?WriteConcern $writeConcern = null) : void
531
    {
532
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
533
            assert($class instanceof ClassMetadata);
534
            if ($class->isMappedSuperclass || ! $class->isSharded()) {
535
                continue;
536
            }
537
538
            $this->ensureDocumentSharding($class->name, $writeConcern);
539
        }
540
    }
541
542
    /**
543
     * Ensure sharding for collection by document name.
544
     *
545
     * @throws MongoDBException
546
     */
547 11
    public function ensureDocumentSharding(string $documentName, ?WriteConcern $writeConcern = null) : void
548
    {
549 11
        $class = $this->dm->getClassMetadata($documentName);
550 11
        if (! $class->isSharded()) {
551
            return;
552
        }
553
554 11
        if ($this->collectionIsSharded($documentName)) {
555 1
            return;
556
        }
557
558 11
        $this->enableShardingForDbByDocumentName($documentName);
559
560
        try {
561 11
            $this->runShardCollectionCommand($documentName, $writeConcern);
562 1
        } catch (RuntimeException $e) {
563 1
            throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage());
564
        }
565 10
    }
566
567
    /**
568
     * Enable sharding for database which contains documents with given name.
569
     *
570
     * @throws MongoDBException
571
     */
572 11
    public function enableShardingForDbByDocumentName(string $documentName) : void
573
    {
574 11
        $dbName  = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
575 11
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
576
577
        try {
578 11
            $adminDb->command(['enableSharding' => $dbName]);
579
        } catch (ServerException $e) {
580
            // Don't throw an exception if sharding is already enabled; there's just no other way to check this
581
            if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) {
582
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
583
            }
584
        } catch (RuntimeException $e) {
585
            throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
586
        }
587 11
    }
588
589 11
    private function runShardCollectionCommand(string $documentName, ?WriteConcern $writeConcern = null) : array
590
    {
591 11
        $class    = $this->dm->getClassMetadata($documentName);
592 11
        $dbName   = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
593 11
        $shardKey = $class->getShardKey();
594 11
        $adminDb  = $this->dm->getClient()->selectDatabase('admin');
595
596 11
        return $adminDb->command(
597 11
            array_merge(
598
                [
599 11
                    'shardCollection' => $dbName . '.' . $class->getCollection(),
600 11
                    'key'             => $shardKey['keys'],
601
                ],
602 11
                $this->getWriteOptions(null, $writeConcern)
603
            )
604 10
        )->toArray()[0];
605
    }
606
607 8
    private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
608
    {
609 8
        $this->ensureChunksIndex($class, $maxTimeMs, $writeConcern);
610 8
        $this->ensureFilesIndex($class, $maxTimeMs, $writeConcern);
611 8
    }
612
613 8
    private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
614
    {
615 8
        $chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection();
616 8
        foreach ($chunksCollection->listIndexes() as $index) {
617
            if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) {
618
                return;
619
            }
620
        }
621
622 8
        $chunksCollection->createIndex(
623 8
            self::GRIDFS_FILE_COLLECTION_INDEX,
624 8
            $this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true])
625
        );
626 8
    }
627
628 8
    private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void
629
    {
630 8
        $filesCollection = $this->dm->getDocumentCollection($class->getName());
631 8
        foreach ($filesCollection->listIndexes() as $index) {
632
            if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) {
633
                return;
634
            }
635
        }
636
637 8
        $filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern));
638 8
    }
639
640 11
    private function collectionIsSharded(string $documentName) : bool
641
    {
642 11
        $class = $this->dm->getClassMetadata($documentName);
643
644 11
        $database    = $this->dm->getDocumentDatabase($documentName);
645 11
        $collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]);
646 11
        if (! iterator_count($collections)) {
647 7
            return false;
648
        }
649
650 4
        $stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0];
651 4
        return (bool) ($stats['sharded'] ?? false);
652
    }
653
654 86
    private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []) : array
655
    {
656 86
        unset($options['maxTimeMs'], $options['writeConcern']);
657
658 86
        if ($maxTimeMs !== null) {
659 32
            $options['maxTimeMs'] = $maxTimeMs;
660
        }
661
662 86
        if ($writeConcern !== null) {
663 32
            $options['writeConcern'] = $writeConcern;
664
        }
665
666 86
        return $options;
667
    }
668
}
669