Completed
Push — master ( 11f27c...4713a5 )
by Andreas
08:33
created

SchemaManager::ensureDocumentIndexes()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 8.0291

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 12
cts 13
cp 0.9231
rs 7.7777
c 0
b 0
f 0
cc 8
eloc 12
nc 5
nop 2
crap 8.0291
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ODM\MongoDB;
21
22
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
23
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory;
24
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
25
use MongoDB\Driver\Exception\RuntimeException;
26
use MongoDB\Driver\WriteConcern;
27
use MongoDB\Model\IndexInfo;
28
29
class SchemaManager
30
{
31
    /**
32
     * @var DocumentManager
33
     */
34
    protected $dm;
35
36
    /**
37
     *
38
     * @var ClassMetadataFactory
39
     */
40
    protected $metadataFactory;
41
42
    /**
43
     * @param DocumentManager $dm
44
     * @param ClassMetadataFactory $cmf
45
     */
46 1681
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
47
    {
48 1681
        $this->dm = $dm;
49 1681
        $this->metadataFactory = $cmf;
50 1681
    }
51
52
    /**
53
     * Ensure indexes are created for all documents that can be loaded with the
54
     * metadata factory.
55
     *
56
     * @param integer $timeout Timeout (ms) for acknowledged index creation
57
     */
58 1 View Code Duplication
    public function ensureIndexes($timeout = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
59
    {
60 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
61 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
62 1
                continue;
63
            }
64 1
            $this->ensureDocumentIndexes($class->name, $timeout);
65
        }
66 1
    }
67
68
    /**
69
     * Ensure indexes exist for all mapped document classes.
70
     *
71
     * Indexes that exist in MongoDB but not the document metadata will be
72
     * deleted.
73
     *
74
     * @param integer $timeout Timeout (ms) for acknowledged index creation
75
     */
76 View Code Duplication
    public function updateIndexes($timeout = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
77
    {
78
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
79
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
80
                continue;
81
            }
82
            $this->updateDocumentIndexes($class->name, $timeout);
83
        }
84
    }
85
86
    /**
87
     * Ensure indexes exist for the mapped document class.
88
     *
89
     * Indexes that exist in MongoDB but not the document metadata will be
90
     * deleted.
91
     *
92
     * @param string $documentName
93
     * @param integer $timeout Timeout (ms) for acknowledged index creation
94
     * @throws \InvalidArgumentException
95
     */
96 3
    public function updateDocumentIndexes($documentName, $timeout = null)
97
    {
98 3
        $class = $this->dm->getClassMetadata($documentName);
99
100 3
        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 3
        $documentIndexes = $this->getDocumentIndexes($documentName);
105 3
        $collection = $this->dm->getDocumentCollection($documentName);
106 3
        $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 3
        $self = $this;
112 3
        $mongoIndexes = array_filter($mongoIndexes, function (IndexInfo $mongoIndex) use ($documentIndexes, $self) {
113 1
            if ('_id_' === $mongoIndex['name']) {
114
                return false;
115
            }
116
117 1
            foreach ($documentIndexes as $documentIndex) {
118 1
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
119 1
                    return false;
120
                }
121
            }
122
123 1
            return true;
124 3
        });
125
126
        // Delete indexes that do not exist in class metadata
127 3
        foreach ($mongoIndexes as $mongoIndex) {
128 1
            if (! isset($mongoIndex['name'])) {
129
                continue;
130
            }
131
132 1
            $collection->dropIndex($mongoIndex['name']);
133
        }
134
135 3
        $this->ensureDocumentIndexes($documentName, $timeout);
136 3
    }
137
138
    /**
139
     * @param string $documentName
140
     * @return array
141
     */
142 19
    public function getDocumentIndexes($documentName)
143
    {
144 19
        $visited = array();
145 19
        return $this->doGetDocumentIndexes($documentName, $visited);
146
    }
147
148
    /**
149
     * @param string $documentName
150
     * @param array $visited
151
     * @return array
152
     */
153 19
    private function doGetDocumentIndexes($documentName, array &$visited)
154
    {
155 19
        if (isset($visited[$documentName])) {
156 1
            return array();
157
        }
158
159 19
        $visited[$documentName] = true;
160
161 19
        $class = $this->dm->getClassMetadata($documentName);
162 19
        $indexes = $this->prepareIndexes($class);
163 19
        $embeddedDocumentIndexes = array();
164
165
        // Add indexes from embedded & referenced documents
166 19
        foreach ($class->fieldMappings as $fieldMapping) {
167 19
            if (isset($fieldMapping['embedded'])) {
168 3
                if (isset($fieldMapping['targetDocument'])) {
169 3
                    $possibleEmbeds = array($fieldMapping['targetDocument']);
170 2
                } elseif (isset($fieldMapping['discriminatorMap'])) {
171 1
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
172
                } else {
173 1
                    continue;
174
                }
175 3
                foreach ($possibleEmbeds as $embed) {
176 3
                    if (isset($embeddedDocumentIndexes[$embed])) {
177 2
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
178
                    } else {
179 3
                        $embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited);
180 3
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
181
                    }
182 3
                    foreach ($embeddedIndexes as $embeddedIndex) {
183 2
                        foreach ($embeddedIndex['keys'] as $key => $value) {
184 2
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
185 2
                            unset($embeddedIndex['keys'][$key]);
186
                        }
187 3
                        $indexes[] = $embeddedIndex;
188
                    }
189
                }
190 19
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
191 8
                foreach ($indexes as $idx => $index) {
192 8
                    $newKeys = array();
193 8
                    foreach ($index['keys'] as $key => $v) {
194 8
                        if ($key == $fieldMapping['name']) {
195 2
                            $key = ClassMetadataInfo::getReferenceFieldName($fieldMapping['storeAs'], $key);
196
                        }
197 8
                        $newKeys[$key] = $v;
198
                    }
199 19
                    $indexes[$idx]['keys'] = $newKeys;
200
                }
201
            }
202
        }
203 19
        return $indexes;
204
    }
205
206
    /**
207
     * @param ClassMetadata $class
208
     * @return array
209
     */
210 19
    private function prepareIndexes(ClassMetadata $class)
211
    {
212 19
        $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
213 19
        $indexes = $class->getIndexes();
214 19
        $newIndexes = array();
215
216 19
        foreach ($indexes as $index) {
217
            $newIndex = array(
218 19
                'keys' => array(),
219 19
                'options' => $index['options']
220
            );
221 19
            foreach ($index['keys'] as $key => $value) {
222 19
                $key = $persister->prepareFieldName($key);
223 19
                if ($class->hasField($key)) {
224 17
                    $mapping = $class->getFieldMapping($key);
225 17
                    $newIndex['keys'][$mapping['name']] = $value;
226
                } else {
227 19
                    $newIndex['keys'][$key] = $value;
228
                }
229
            }
230
231 19
            $newIndexes[] = $newIndex;
232
        }
233
234 19
        return $newIndexes;
235
    }
236
237
    /**
238
     * Ensure the given document's indexes are created.
239
     *
240
     * @param string $documentName
241
     * @param integer $timeout Timeout (ms) for acknowledged index creation
242
     * @throws \InvalidArgumentException
243
     */
244 15
    public function ensureDocumentIndexes($documentName, $timeout = null)
245
    {
246 15
        $class = $this->dm->getClassMetadata($documentName);
247 15
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
248
            throw new \InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
249
        }
250 15
        if ($indexes = $this->getDocumentIndexes($documentName)) {
251 15
            $collection = $this->dm->getDocumentCollection($class->name);
252 15
            foreach ($indexes as $index) {
253 15
                $keys = $index['keys'];
254 15
                $options = $index['options'];
255
256 15
                if ( ! isset($options['timeout']) && isset($timeout)) {
257 1
                    $options['timeout'] = $timeout;
258
                }
259
260 15
                $collection->createIndex($keys, $options);
261
            }
262
        }
263 15
    }
264
265
    /**
266
     * Delete indexes for all documents that can be loaded with the
267
     * metadata factory.
268
     */
269 1 View Code Duplication
    public function deleteIndexes()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
270
    {
271 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
272 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
273 1
                continue;
274
            }
275 1
            $this->deleteDocumentIndexes($class->name);
276
        }
277 1
    }
278
279
    /**
280
     * Delete the given document's indexes.
281
     *
282
     * @param string $documentName
283
     * @throws \InvalidArgumentException
284
     */
285 2 View Code Duplication
    public function deleteDocumentIndexes($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
286
    {
287 2
        $class = $this->dm->getClassMetadata($documentName);
288 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
289
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
290
        }
291 2
        $this->dm->getDocumentCollection($documentName)->dropIndexes();
292 2
    }
293
294
    /**
295
     * Create all the mapped document collections in the metadata factory.
296
     */
297 1 View Code Duplication
    public function createCollections()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
    {
299 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
300 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
301 1
                continue;
302
            }
303 1
            $this->createDocumentCollection($class->name);
304
        }
305 1
    }
306
307
    /**
308
     * Create the document collection for a mapped class.
309
     *
310
     * @param string $documentName
311
     * @throws \InvalidArgumentException
312
     */
313 3
    public function createDocumentCollection($documentName)
314
    {
315 3
        $class = $this->dm->getClassMetadata($documentName);
316
317 3
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
318
            throw new \InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
319
        }
320
321 3
        $this->dm->getDocumentDatabase($documentName)->createCollection(
322 3
            $class->getCollection(),
323
            [
324 3
                'capped' => $class->getCollectionCapped(),
325 3
                'size' => $class->getCollectionSize(),
326 3
                'max' => $class->getCollectionMax(),
327
            ]
328
        );
329 3
    }
330
331
    /**
332
     * Drop all the mapped document collections in the metadata factory.
333
     */
334 1 View Code Duplication
    public function dropCollections()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
335
    {
336 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
337 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
338 1
                continue;
339
            }
340 1
            $this->dropDocumentCollection($class->name);
341
        }
342 1
    }
343
344
    /**
345
     * Drop the document collection for a mapped class.
346
     *
347
     * @param string $documentName
348
     * @throws \InvalidArgumentException
349
     */
350 3 View Code Duplication
    public function dropDocumentCollection($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
351
    {
352 3
        $class = $this->dm->getClassMetadata($documentName);
353 3
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
354
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
355
        }
356 3
        $this->dm->getDocumentCollection($documentName)->drop();
357 3
    }
358
359
    /**
360
     * Drop all the mapped document databases in the metadata factory.
361
     */
362 1 View Code Duplication
    public function dropDatabases()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
363
    {
364 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
365 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
366 1
                continue;
367
            }
368 1
            $this->dropDocumentDatabase($class->name);
369
        }
370 1
    }
371
372
    /**
373
     * Drop the document database for a mapped class.
374
     *
375
     * @param string $documentName
376
     * @throws \InvalidArgumentException
377
     */
378 2 View Code Duplication
    public function dropDocumentDatabase($documentName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
379
    {
380 2
        $class = $this->dm->getClassMetadata($documentName);
381 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
382
            throw new \InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
383
        }
384 2
        $this->dm->getDocumentDatabase($documentName)->drop();
385 2
    }
386
387
    /**
388
     * Create all the mapped document databases in the metadata factory.
389
     *
390
     * @deprecated Databases are created automatically by MongoDB (>= 3.0). Deprecated since ODM 1.2, to be removed in ODM 2.0.
391
     */
392
    public function createDatabases()
393
    {
394
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
395
            sprintf('%s was deprecated in version 1.2 - databases are created automatically by MongoDB (>= 3.0).', __METHOD__),
396
            E_USER_DEPRECATED
397
        );
398
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
399
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
400
                continue;
401
            }
402
            $this->createDocumentDatabase($class->name);
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\ODM\MongoDB\Sch...reateDocumentDatabase() has been deprecated with message: A database is created automatically by MongoDB (>= 3.0). Deprecated since ODM 1.2, to be removed in ODM 2.0.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
403
        }
404
    }
405
406
    /**
407
     * Create the document database for a mapped class.
408
     *
409
     * @param string $documentName
410
     * @throws \InvalidArgumentException
411
     *
412
     * @deprecated A database is created automatically by MongoDB (>= 3.0). Deprecated since ODM 1.2, to be removed in ODM 2.0.
413
     */
414
    public function createDocumentDatabase($documentName)
415
    {
416
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
417
            sprintf('%s was deprecated in version 1.2 - databases are created automatically by MongoDB (>= 3.0).', __METHOD__),
418
            E_USER_DEPRECATED
419
        );
420
        $class = $this->dm->getClassMetadata($documentName);
421
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
422
            throw new \InvalidArgumentException('Cannot create databases for mapped super classes, embedded documents or query result documents.');
423
        }
424
425
        $this->dm->getDocumentDatabase($documentName)->execute('function() { return true; }');
0 ignored issues
show
Bug introduced by
The method execute() does not seem to exist on object<MongoDB\Database>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
426
    }
427
428
    /**
429
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
430
     * considered equivalent to an index in class metadata.
431
     *
432
     * Indexes are considered different if:
433
     *
434
     *   (a) Key/direction pairs differ or are not in the same order
435
     *   (b) Sparse or unique options differ
436
     *   (c) Mongo index is unique without dropDups and mapped index is unique
437
     *       with dropDups
438
     *   (d) Geospatial options differ (bits, max, min)
439
     *   (e) The partialFilterExpression differs
440
     *
441
     * Regarding (c), the inverse case is not a reason to delete and
442
     * recreate the index, since dropDups only affects creation of
443
     * the unique index. Additionally, the background option is only
444
     * relevant to index creation and is not considered.
445
     *
446
     * @param array|IndexInfo $mongoIndex Mongo index data.
447
     * @param array $documentIndex Document index data.
448
     * @return bool True if the indexes are equivalent, otherwise false.
449
     */
450 30
    public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)
451
    {
452 30
        $documentIndexOptions = $documentIndex['options'];
453
454 30
        if ($mongoIndex['key'] != $documentIndex['keys']) {
455 2
            return false;
456
        }
457
458 28
        if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) {
459 2
            return false;
460
        }
461
462 26
        if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) {
463 2
            return false;
464
        }
465
466 24
        if ( ! empty($mongoIndex['unique']) && empty($mongoIndex['dropDups']) &&
467 24
            ! empty($documentIndexOptions['unique']) && ! empty($documentIndexOptions['dropDups'])) {
468
469 1
            return false;
470
        }
471
472 23
        foreach (array('bits', 'max', 'min') as $option) {
473 23
            if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) {
474 6
                return false;
475
            }
476
477 21
            if (isset($mongoIndex[$option]) && isset($documentIndexOptions[$option]) &&
478 21
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
479
480 21
                return false;
481
            }
482
        }
483
484 14
        if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
485 2
            return false;
486
        }
487
488 12
        if (isset($mongoIndex['partialFilterExpression']) && isset($documentIndexOptions['partialFilterExpression']) &&
489 12
            $mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
490
491 1
            return false;
492
        }
493
494 11
        return true;
495
    }
496
497
    /**
498
     * Ensure collections are sharded for all documents that can be loaded with the
499
     * metadata factory.
500
     *
501
     * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections
502
     *
503
     * @throws MongoDBException
504
     */
505
    public function ensureSharding(array $indexOptions = array())
506
    {
507
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
508
            if ($class->isMappedSuperclass || !$class->isSharded()) {
509
                continue;
510
            }
511
512
            $this->ensureDocumentSharding($class->name, $indexOptions);
513
        }
514
    }
515
516
    /**
517
     * Ensure sharding for collection by document name.
518
     *
519
     * @param string $documentName
520
     * @param array  $indexOptions Options for `ensureIndex` command. It's performed on an existing collections.
521
     *
522
     * @throws MongoDBException
523
     */
524 2
    public function ensureDocumentSharding($documentName, array $indexOptions = array())
525
    {
526 2
        $class = $this->dm->getClassMetadata($documentName);
527 2
        if ( ! $class->isSharded()) {
528
            return;
529
        }
530
531 2
        $this->enableShardingForDbByDocumentName($documentName);
532
533 2
        $try = 0;
534
        do {
535
            try {
536 2
                $result = $this->runShardCollectionCommand($documentName);
537 2
                $done = true;
538
539
                // Need to check error message because MongoDB 3.0 does not return a code for this error
540 2
                if ($result['ok'] != 1 && strpos($result['errmsg'], 'please create an index that starts') !== false) {
541
                    // The proposed key is not returned when using mongo-php-adapter with ext-mongodb.
542
                    // See https://github.com/mongodb/mongo-php-driver/issues/296 for details
543
                    if (isset($result['proposedKey'])) {
544
                        $key = $result['proposedKey'];
545
                    } else {
546
                        $key = $this->dm->getClassMetadata($documentName)->getShardKey()['keys'];
547
                    }
548
549
                    $this->dm->getDocumentCollection($documentName)->ensureIndex($key, $indexOptions);
0 ignored issues
show
Bug introduced by
The method ensureIndex() does not seem to exist on object<MongoDB\Collection>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
550 2
                    $done = false;
551
                }
552 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...
553 1
                if ($e->getCode() === 20 || $e->getMessage() == 'already sharded') {
554 1
                    return;
555
                }
556
557
                throw $e;
558
            }
559 2
        } while (! $done && $try < 2);
0 ignored issues
show
introduced by
It seems like the condition $try < 2 is always satisfied by any possible value of $try. Are you sure you do not have a deadlock here?
Loading history...
560
561
        // Starting with MongoDB 3.2, this command returns code 20 when a collection is already sharded.
562
        // For older MongoDB versions, check the error message
563 2
        if ($result['ok'] == 1 || (isset($result['code']) && $result['code'] == 20) || $result['errmsg'] == 'already sharded') {
564 2
            return;
565
        }
566
567
        throw MongoDBException::failedToEnsureDocumentSharding($documentName, $result['errmsg']);
568
    }
569
570
    /**
571
     * Enable sharding for database which contains documents with given name.
572
     *
573
     * @param string $documentName
574
     *
575
     * @throws MongoDBException
576
     */
577 2
    public function enableShardingForDbByDocumentName($documentName)
578
    {
579 2
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
580 2
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
581
582
        try {
583 2
            $adminDb->command(array('enableSharding' => $dbName));
584 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...
585 1
            if ($e->getCode() !== 23 || $e->getMessage() === 'already enabled') {
586
                throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage());
587
            }
588
        }
589 2
    }
590
591
    /**
592
     * @param $documentName
593
     *
594
     * @return array
595
     */
596 2
    private function runShardCollectionCommand($documentName)
597
    {
598 2
        $class = $this->dm->getClassMetadata($documentName);
599 2
        $dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName();
600 2
        $shardKey = $class->getShardKey();
601 2
        $adminDb = $this->dm->getClient()->selectDatabase('admin');
602
603 2
        $result = $adminDb->command(
604
            array(
605 2
                'shardCollection' => $dbName . '.' . $class->getCollection(),
606 2
                'key'             => $shardKey['keys']
607
            )
608 2
        )->toArray()[0];
609
610 2
        return $result;
611
    }
612
}
613