Completed
Pull Request — master (#1693)
by
unknown
07:50
created

SchemaManager::runShardCollectionCommand()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5.0061

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 15
cts 16
cp 0.9375
rs 8.439
c 0
b 0
f 0
cc 5
eloc 18
nc 6
nop 1
crap 5.0061
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 Doctrine\ODM\MongoDB\Mapping\MappingException;
26
27
class SchemaManager
28
{
29
    /**
30
     * @var DocumentManager
31
     */
32
    protected $dm;
33
34
    /**
35
     *
36
     * @var ClassMetadataFactory
37
     */
38
    protected $metadataFactory;
39
40
    /**
41
     * @param DocumentManager $dm
42
     * @param ClassMetadataFactory $cmf
43
     */
44 1184
    public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf)
45
    {
46 1184
        $this->dm = $dm;
47 1184
        $this->metadataFactory = $cmf;
48 1184
    }
49
50
    /**
51
     * Ensure indexes are created for all documents that can be loaded with the
52
     * metadata factory.
53
     *
54
     * @param integer $timeout Timeout (ms) for acknowledged index creation
55
     */
56 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...
57
    {
58 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
59 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
60 1
                continue;
61
            }
62 1
            $this->ensureDocumentIndexes($class->name, $timeout);
63
        }
64 1
    }
65
66
    /**
67
     * Ensure indexes exist for all mapped document classes.
68
     *
69
     * Indexes that exist in MongoDB but not the document metadata will be
70
     * deleted.
71
     *
72
     * @param integer $timeout Timeout (ms) for acknowledged index creation
73
     */
74 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...
75
    {
76
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
77
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
78
                continue;
79
            }
80
            $this->updateDocumentIndexes($class->name, $timeout);
81
        }
82
    }
83
84
    /**
85
     * Ensure indexes exist for the mapped document class.
86
     *
87
     * Indexes that exist in MongoDB but not the document metadata will be
88
     * deleted.
89
     *
90
     * @param string $documentName
91
     * @param integer $timeout Timeout (ms) for acknowledged index creation
92
     * @throws \InvalidArgumentException
93
     */
94 3
    public function updateDocumentIndexes($documentName, $timeout = null)
95
    {
96 3
        $class = $this->dm->getClassMetadata($documentName);
97
98 3
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
99
            throw new \InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.');
100
        }
101
102 3
        $documentIndexes = $this->getDocumentIndexes($documentName);
103 3
        $collection = $this->dm->getDocumentCollection($documentName);
104 3
        $mongoIndexes = $collection->getIndexInfo();
105
106
        /* Determine which Mongo indexes should be deleted. Exclude the ID index
107
         * and those that are equivalent to any in the class metadata.
108
         */
109 3
        $self = $this;
110 3
        $mongoIndexes = array_filter($mongoIndexes, function ($mongoIndex) use ($documentIndexes, $self) {
111 1
            if ('_id_' === $mongoIndex['name']) {
112
                return false;
113
            }
114
115 1
            foreach ($documentIndexes as $documentIndex) {
116 1
                if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) {
117 1
                    return false;
118
                }
119
            }
120
121 1
            return true;
122 3
        });
123
124
        // Delete indexes that do not exist in class metadata
125 3
        foreach ($mongoIndexes as $mongoIndex) {
126 1
            if (isset($mongoIndex['name'])) {
127
                /* Note: MongoCollection::deleteIndex() cannot delete
128
                 * custom-named indexes, so use the deleteIndexes command.
129
                 */
130 1
                $collection->getDatabase()->command(array(
131 1
                    'deleteIndexes' => $collection->getName(),
132 1
                    'index' => $mongoIndex['name'],
133
                ));
134
            }
135
        }
136
137 3
        $this->ensureDocumentIndexes($documentName, $timeout);
138 3
    }
139
140
    /**
141
     * @param string $documentName
142
     * @return array
143
     */
144 45
    public function getDocumentIndexes($documentName)
145
    {
146 45
        $visited = array();
147 45
        return $this->doGetDocumentIndexes($documentName, $visited);
148
    }
149
150
    /**
151
     * @param string $documentName
152
     * @param array $visited
153
     * @return array
154
     */
155 45
    private function doGetDocumentIndexes($documentName, array &$visited)
156
    {
157 45
        if (isset($visited[$documentName])) {
158 1
            return array();
159
        }
160
161 45
        $visited[$documentName] = true;
162
163 45
        $class = $this->dm->getClassMetadata($documentName);
164 45
        $indexes = $this->prepareIndexes($class);
165 45
        $embeddedDocumentIndexes = array();
166
167
        // Add indexes from embedded & referenced documents
168 45
        foreach ($class->fieldMappings as $fieldMapping) {
169 45
            if (isset($fieldMapping['embedded'])) {
170 28
                if (isset($fieldMapping['targetDocument'])) {
171 28
                    $possibleEmbeds = array($fieldMapping['targetDocument']);
172 2
                } elseif (isset($fieldMapping['discriminatorMap'])) {
173 1
                    $possibleEmbeds = array_unique($fieldMapping['discriminatorMap']);
174
                } else {
175 1
                    continue;
176
                }
177 28
                foreach ($possibleEmbeds as $embed) {
178 28
                    if (isset($embeddedDocumentIndexes[$embed])) {
179 22
                        $embeddedIndexes = $embeddedDocumentIndexes[$embed];
180
                    } else {
181 28
                        $embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited);
182 28
                        $embeddedDocumentIndexes[$embed] = $embeddedIndexes;
183
                    }
184 28
                    foreach ($embeddedIndexes as $embeddedIndex) {
185 22
                        foreach ($embeddedIndex['keys'] as $key => $value) {
186 22
                            $embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value;
187 22
                            unset($embeddedIndex['keys'][$key]);
188
                        }
189 28
                        $indexes[] = $embeddedIndex;
190
                    }
191
                }
192 45
            } elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) {
193 28
                foreach ($indexes as $idx => $index) {
194 28
                    $newKeys = array();
195 28
                    foreach ($index['keys'] as $key => $v) {
196 28
                        if ($key == $fieldMapping['name']) {
197 2
                            $key = ClassMetadataInfo::getReferenceFieldName($fieldMapping['storeAs'], $key);
198
                        }
199 28
                        $newKeys[$key] = $v;
200
                    }
201 45
                    $indexes[$idx]['keys'] = $newKeys;
202
                }
203
            }
204
        }
205 45
        return $indexes;
206
    }
207
208
    /**
209
     * @param ClassMetadata $class
210
     * @return array
211
     */
212 45
    private function prepareIndexes(ClassMetadata $class)
213
    {
214 45
        $persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name);
215 45
        $indexes = $class->getIndexes();
216 45
        $newIndexes = array();
217
218 45
        foreach ($indexes as $index) {
219
            $newIndex = array(
220 45
                'keys' => array(),
221 45
                'options' => $index['options']
222
            );
223 45
            foreach ($index['keys'] as $key => $value) {
224 45
                $key = $persister->prepareFieldName($key);
225 45
                if ($class->hasField($key)) {
226 43
                    $mapping = $class->getFieldMapping($key);
227 43
                    $newIndex['keys'][$mapping['name']] = $value;
228
                } else {
229 45
                    $newIndex['keys'][$key] = $value;
230
                }
231
            }
232
233 45
            $newIndexes[] = $newIndex;
234
        }
235
236 45
        return $newIndexes;
237
    }
238
239
    /**
240
     * Ensure the given document's indexes are created.
241
     *
242
     * @param string $documentName
243
     * @param integer $timeout Timeout (ms) for acknowledged index creation
244
     * @throws \InvalidArgumentException
245
     */
246 41
    public function ensureDocumentIndexes($documentName, $timeout = null)
247
    {
248 41
        $class = $this->dm->getClassMetadata($documentName);
249 41
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
250
            throw new \InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.');
251
        }
252 41
        if ($indexes = $this->getDocumentIndexes($documentName)) {
253 41
            $collection = $this->dm->getDocumentCollection($class->name);
254 41
            foreach ($indexes as $index) {
255 41
                $keys = $index['keys'];
256 41
                $options = $index['options'];
257
258 41
                if ( ! isset($options['safe']) && ! isset($options['w'])) {
259 40
                    $options['w'] = 1;
260
                }
261
262 41
                if (isset($options['safe']) && ! isset($options['w'])) {
263 1
                    $options['w'] = is_bool($options['safe']) ? (integer) $options['safe'] : $options['safe'];
264 1
                    unset($options['safe']);
265
                }
266
267 41
                if ( ! isset($options['timeout']) && isset($timeout)) {
268 1
                    $options['timeout'] = $timeout;
269
                }
270
271 41
                $collection->ensureIndex($keys, $options);
272
            }
273
        }
274 41
    }
275
276
    /**
277
     * Delete indexes for all documents that can be loaded with the
278
     * metadata factory.
279
     */
280 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...
281
    {
282 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
283 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
284 1
                continue;
285
            }
286 1
            $this->deleteDocumentIndexes($class->name);
287
        }
288 1
    }
289
290
    /**
291
     * Delete the given document's indexes.
292
     *
293
     * @param string $documentName
294
     * @throws \InvalidArgumentException
295
     */
296 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...
297
    {
298 2
        $class = $this->dm->getClassMetadata($documentName);
299 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
300
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
301
        }
302 2
        $this->dm->getDocumentCollection($documentName)->deleteIndexes();
303 2
    }
304
305
    /**
306
     * Create all the mapped document collections in the metadata factory.
307
     */
308 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...
309
    {
310 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
311 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
312 1
                continue;
313
            }
314 1
            $this->createDocumentCollection($class->name);
315
        }
316 1
    }
317
318
    /**
319
     * Create the document collection for a mapped class.
320
     *
321
     * @param string $documentName
322
     * @throws \InvalidArgumentException
323
     */
324 4
    public function createDocumentCollection($documentName)
325
    {
326 4
        $class = $this->dm->getClassMetadata($documentName);
327
328 4
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
329
            throw new \InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.');
330
        }
331
332 4
        if ($class->isFile()) {
333 2
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getCollection() . '.files');
334 2
            $this->dm->getDocumentDatabase($documentName)->createCollection($class->getCollection() . '.chunks');
335
336 2
            return;
337
        }
338
339 3
        $this->dm->getDocumentDatabase($documentName)->createCollection(
340 3
            $class->getCollection(),
341 3
            $class->getCollectionCapped(),
342 3
            $class->getCollectionSize(),
343 3
            $class->getCollectionMax()
344
        );
345 3
    }
346
347
    /**
348
     * Drop all the mapped document collections in the metadata factory.
349
     */
350 2 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...
351
    {
352 2
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
353 2
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
354 2
                continue;
355
            }
356 2
            $this->dropDocumentCollection($class->name);
357
        }
358 2
    }
359
360
    /**
361
     * Drop the document collection for a mapped class.
362
     *
363
     * @param string $documentName
364
     * @throws \InvalidArgumentException
365
     */
366 4 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...
367
    {
368 4
        $class = $this->dm->getClassMetadata($documentName);
369 4
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
370
            throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.');
371
        }
372 4
        $this->dm->getDocumentCollection($documentName)->drop();
373 4
    }
374
375
    /**
376
     * Drop all the mapped document databases in the metadata factory.
377
     */
378 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...
379
    {
380 1
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
381 1
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
382 1
                continue;
383
            }
384 1
            $this->dropDocumentDatabase($class->name);
385
        }
386 1
    }
387
388
    /**
389
     * Drop the document database for a mapped class.
390
     *
391
     * @param string $documentName
392
     * @throws \InvalidArgumentException
393
     */
394 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...
395
    {
396 2
        $class = $this->dm->getClassMetadata($documentName);
397 2
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
398
            throw new \InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.');
399
        }
400 2
        $this->dm->getDocumentDatabase($documentName)->drop();
401 2
    }
402
403
    /**
404
     * Create all the mapped document databases in the metadata factory.
405
     *
406
     * @deprecated Databases are created automatically by MongoDB (>= 3.0). Deprecated since ODM 1.2, to be removed in ODM 2.0.
407
     */
408
    public function createDatabases()
409
    {
410
        @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...
411
            sprintf('%s was deprecated in version 1.2 - databases are created automatically by MongoDB (>= 3.0).', __METHOD__),
412
            E_USER_DEPRECATED
413
        );
414
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
415
            if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
416
                continue;
417
            }
418
            $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...
419
        }
420
    }
421
422
    /**
423
     * Create the document database for a mapped class.
424
     *
425
     * @param string $documentName
426
     * @throws \InvalidArgumentException
427
     *
428
     * @deprecated A database is created automatically by MongoDB (>= 3.0). Deprecated since ODM 1.2, to be removed in ODM 2.0.
429
     */
430
    public function createDocumentDatabase($documentName)
431
    {
432
        @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...
433
            sprintf('%s was deprecated in version 1.2 - databases are created automatically by MongoDB (>= 3.0).', __METHOD__),
434
            E_USER_DEPRECATED
435
        );
436
        $class = $this->dm->getClassMetadata($documentName);
437
        if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) {
438
            throw new \InvalidArgumentException('Cannot create databases for mapped super classes, embedded documents or query result documents.');
439
        }
440
441
        $this->dm->getDocumentDatabase($documentName)->execute('function() { return true; }');
442
    }
443
444
    /**
445
     * Determine if an index returned by MongoCollection::getIndexInfo() can be
446
     * considered equivalent to an index in class metadata.
447
     *
448
     * Indexes are considered different if:
449
     *
450
     *   (a) Key/direction pairs differ or are not in the same order
451
     *   (b) Sparse or unique options differ
452
     *   (c) Mongo index is unique without dropDups and mapped index is unique
453
     *       with dropDups
454
     *   (d) Geospatial options differ (bits, max, min)
455
     *   (e) The partialFilterExpression differs
456
     *
457
     * Regarding (c), the inverse case is not a reason to delete and
458
     * recreate the index, since dropDups only affects creation of
459
     * the unique index. Additionally, the background option is only
460
     * relevant to index creation and is not considered.
461
     *
462
     * @param array $mongoIndex Mongo index data.
463
     * @param array $documentIndex Document index data.
464
     * @return bool True if the indexes are equivalent, otherwise false.
465
     */
466 30
    public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)
467
    {
468 30
        $documentIndexOptions = $documentIndex['options'];
469
470 30
        if ($mongoIndex['key'] != $documentIndex['keys']) {
471 2
            return false;
472
        }
473
474 28
        if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) {
475 2
            return false;
476
        }
477
478 26
        if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) {
479 2
            return false;
480
        }
481
482 24
        if ( ! empty($mongoIndex['unique']) && empty($mongoIndex['dropDups']) &&
483 24
            ! empty($documentIndexOptions['unique']) && ! empty($documentIndexOptions['dropDups'])) {
484
485 1
            return false;
486
        }
487
488 23
        foreach (array('bits', 'max', 'min') as $option) {
489 23
            if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) {
490 6
                return false;
491
            }
492
493 21
            if (isset($mongoIndex[$option]) && isset($documentIndexOptions[$option]) &&
494 21
                $mongoIndex[$option] !== $documentIndexOptions[$option]) {
495
496 21
                return false;
497
            }
498
        }
499
500 14
        if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) {
501 2
            return false;
502
        }
503
504 12
        if (isset($mongoIndex['partialFilterExpression']) && isset($documentIndexOptions['partialFilterExpression']) &&
505 12
            $mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) {
506
507 1
            return false;
508
        }
509
510 11
        return true;
511
    }
512
513
    /**
514
     * Ensure collections are sharded for all documents that can be loaded with the
515
     * metadata factory.
516
     *
517
     * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections
518
     *
519
     * @throws MongoDBException
520
     */
521
    public function ensureSharding(array $indexOptions = array())
522
    {
523
        foreach ($this->metadataFactory->getAllMetadata() as $class) {
524
            if ($class->isMappedSuperclass || !$class->isSharded()) {
525
                continue;
526
            }
527
528
            $this->ensureDocumentSharding($class->name, $indexOptions);
529
        }
530
    }
531
532
    /**
533
     * Ensure sharding for collection by document name.
534
     *
535
     * @param string $documentName
536
     * @param array  $indexOptions Options for `ensureIndex` command. It's performed on an existing collections.
537
     *
538
     * @throws MongoDBException
539
     */
540 14
    public function ensureDocumentSharding($documentName, array $indexOptions = array())
541
    {
542 14
        $class = $this->dm->getClassMetadata($documentName);
543 14
        if ( ! $class->isSharded()) {
544
            return;
545
        }
546
547 14
        $this->enableShardingForDbByDocumentName($documentName);
548
549 14
        $try = 0;
550
        do {
551 14
            $result = $this->runShardCollectionCommand($documentName);
552 14
            $done = true;
553
554
            // Need to check error message because MongoDB 3.0 does not return a code for this error
555 14
            if ($result['ok'] != 1 && strpos($result['errmsg'], 'please create an index that starts') !== false) {
556
                // The proposed key is not returned when using mongo-php-adapter with ext-mongodb.
557
                // See https://github.com/mongodb/mongo-php-driver/issues/296 for details
558 2
                if (isset($result['proposedKey'])) {
559
                    $key = $result['proposedKey'];
560
                } else {
561 2
                    $key = $this->dm->getClassMetadata($documentName)->getShardKey()['keys'];
562
                }
563
564 2
                $this->dm->getDocumentCollection($documentName)->ensureIndex($key, $indexOptions);
565 2
                $done = false;
566 2
                $try++;
567
            }
568 14
        } while (! $done && $try < 2);
569
570
        // Starting with MongoDB 3.2, this command returns code 20 when a collection is already sharded.
571
        // For older MongoDB versions, check the error message
572 14 View Code Duplication
        if ($result['ok'] == 1 || (isset($result['code']) && $result['code'] == 20) || $result['errmsg'] == 'already sharded') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
573 13
            return;
574
        }
575
576 1
        throw MongoDBException::failedToEnsureDocumentSharding($documentName, $result['errmsg']);
577
    }
578
579
    /**
580
     * Enable sharding for database which contains documents with given name.
581
     *
582
     * @param string $documentName
583
     *
584
     * @throws MongoDBException
585
     */
586 17
    public function enableShardingForDbByDocumentName($documentName)
587
    {
588 17
        $dbName = $this->dm->getDocumentDatabase($documentName)->getName();
589 17
        $adminDb = $this->dm->getConnection()->selectDatabase('admin');
590 17
        $result = $adminDb->command(array('enableSharding' => $dbName));
591
592
        // Error code is only available with MongoDB 3.2. MongoDB 3.0 only returns a message
593
        // Thus, check code if it exists and fall back on error message
594 17 View Code Duplication
        if ($result['ok'] == 1 || (isset($result['code']) && $result['code'] == 23) || $result['errmsg'] == 'already enabled') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
595 16
            return;
596
        }
597
598 1
        throw MongoDBException::failedToEnableSharding($dbName, $result['errmsg']);
599
    }
600
601
    /**
602
     * @param $documentName
603
     *
604
     * @return array
605
     */
606 14
    private function runShardCollectionCommand($documentName)
607
    {
608 14
        $class = $this->dm->getClassMetadata($documentName);
609 14
        $dbName = $this->dm->getDocumentDatabase($documentName)->getName();
610 14
        $shardKey = $class->getShardKey();
611 14
        $adminDb = $this->dm->getConnection()->selectDatabase('admin');
612
613 14
        $shardKeyPart = array();
614 14
        foreach ($shardKey['keys'] as $key => $order) {
615
            try {
616 14
                $mapping = $class->getFieldMappingByDbFieldName($key);
617 14
                if (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
618 14
                    $key = ClassMetadataInfo::getReferenceFieldName($mapping['storeAs'], $key);
619
                }
620
            } catch (MappingException $exception) {
621
                // Ignore hitting unmapped fields
622
            }
623 14
            $shardKeyPart[$key] = $order;
624
        }
625
626 14
        $result = $adminDb->command(
627
            array(
628 14
                'shardCollection' => $dbName . '.' . $class->getCollection(),
629 14
                'key'             => $shardKeyPart
630
            )
631
        );
632
633 14
        return $result;
634
    }
635
}
636