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 MongoDB\Driver\Exception\RuntimeException; |
10
|
|
|
use MongoDB\Driver\Exception\ServerException; |
11
|
|
|
use MongoDB\Model\IndexInfo; |
12
|
|
|
use function array_filter; |
13
|
|
|
use function array_unique; |
14
|
|
|
use function iterator_count; |
15
|
|
|
use function iterator_to_array; |
16
|
|
|
use function ksort; |
17
|
|
|
|
18
|
|
|
class SchemaManager |
19
|
|
|
{ |
20
|
|
|
private const GRIDFS_FILE_COLLECTION_INDEX = ['files_id' => 1, 'n' => 1]; |
21
|
|
|
|
22
|
|
|
private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1]; |
23
|
|
|
|
24
|
|
|
private const CODE_SHARDING_ALREADY_INITIALIZED = 23; |
25
|
|
|
|
26
|
|
|
/** @var DocumentManager */ |
27
|
|
|
protected $dm; |
28
|
|
|
|
29
|
|
|
/** @var ClassMetadataFactory */ |
30
|
|
|
protected $metadataFactory; |
31
|
|
|
|
32
|
1668 |
|
public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf) |
33
|
|
|
{ |
34
|
1668 |
|
$this->dm = $dm; |
35
|
1668 |
|
$this->metadataFactory = $cmf; |
36
|
1668 |
|
} |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Ensure indexes are created for all documents that can be loaded with the |
40
|
|
|
* metadata factory. |
41
|
|
|
*/ |
42
|
1 |
|
public function ensureIndexes(?int $timeout = null): void |
43
|
|
|
{ |
44
|
1 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
45
|
1 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
46
|
1 |
|
continue; |
47
|
|
|
} |
48
|
|
|
|
49
|
1 |
|
$this->ensureDocumentIndexes($class->name, $timeout); |
50
|
|
|
} |
51
|
1 |
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Ensure indexes exist for all mapped document classes. |
55
|
|
|
* |
56
|
|
|
* Indexes that exist in MongoDB but not the document metadata will be |
57
|
|
|
* deleted. |
58
|
|
|
*/ |
59
|
|
|
public function updateIndexes(?int $timeout = null): void |
60
|
|
|
{ |
61
|
|
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
62
|
|
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
63
|
|
|
continue; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$this->updateDocumentIndexes($class->name, $timeout); |
67
|
|
|
} |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Ensure indexes exist for the mapped document class. |
72
|
|
|
* |
73
|
|
|
* Indexes that exist in MongoDB but not the document metadata will be |
74
|
|
|
* deleted. |
75
|
|
|
* |
76
|
|
|
* @throws \InvalidArgumentException |
77
|
|
|
*/ |
78
|
3 |
|
public function updateDocumentIndexes(string $documentName, ?int $timeout = null): void |
79
|
|
|
{ |
80
|
3 |
|
$class = $this->dm->getClassMetadata($documentName); |
81
|
|
|
|
82
|
3 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
83
|
|
|
throw new \InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.'); |
84
|
|
|
} |
85
|
|
|
|
86
|
3 |
|
$documentIndexes = $this->getDocumentIndexes($documentName); |
87
|
3 |
|
$collection = $this->dm->getDocumentCollection($documentName); |
88
|
3 |
|
$mongoIndexes = iterator_to_array($collection->listIndexes()); |
89
|
|
|
|
90
|
|
|
/* Determine which Mongo indexes should be deleted. Exclude the ID index |
91
|
|
|
* and those that are equivalent to any in the class metadata. |
92
|
|
|
*/ |
93
|
3 |
|
$self = $this; |
94
|
|
|
$mongoIndexes = array_filter($mongoIndexes, function (IndexInfo $mongoIndex) use ($documentIndexes, $self) { |
95
|
1 |
|
if ($mongoIndex['name'] === '_id_') { |
96
|
|
|
return false; |
97
|
|
|
} |
98
|
|
|
|
99
|
1 |
|
foreach ($documentIndexes as $documentIndex) { |
100
|
1 |
|
if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) { |
101
|
1 |
|
return false; |
102
|
|
|
} |
103
|
|
|
} |
104
|
|
|
|
105
|
1 |
|
return true; |
106
|
3 |
|
}); |
107
|
|
|
|
108
|
|
|
// Delete indexes that do not exist in class metadata |
109
|
3 |
|
foreach ($mongoIndexes as $mongoIndex) { |
110
|
1 |
|
if (! isset($mongoIndex['name'])) { |
111
|
|
|
continue; |
112
|
|
|
} |
113
|
|
|
|
114
|
1 |
|
$collection->dropIndex($mongoIndex['name']); |
115
|
|
|
} |
116
|
|
|
|
117
|
3 |
|
$this->ensureDocumentIndexes($documentName, $timeout); |
118
|
3 |
|
} |
119
|
|
|
|
120
|
23 |
|
public function getDocumentIndexes(string $documentName): array |
121
|
|
|
{ |
122
|
23 |
|
$visited = []; |
123
|
23 |
|
return $this->doGetDocumentIndexes($documentName, $visited); |
124
|
|
|
} |
125
|
|
|
|
126
|
23 |
|
private function doGetDocumentIndexes(string $documentName, array &$visited): array |
127
|
|
|
{ |
128
|
23 |
|
if (isset($visited[$documentName])) { |
129
|
1 |
|
return []; |
130
|
|
|
} |
131
|
|
|
|
132
|
23 |
|
$visited[$documentName] = true; |
133
|
|
|
|
134
|
23 |
|
$class = $this->dm->getClassMetadata($documentName); |
135
|
23 |
|
$indexes = $this->prepareIndexes($class); |
136
|
23 |
|
$embeddedDocumentIndexes = []; |
137
|
|
|
|
138
|
|
|
// Add indexes from embedded & referenced documents |
139
|
23 |
|
foreach ($class->fieldMappings as $fieldMapping) { |
140
|
23 |
|
if (isset($fieldMapping['embedded'])) { |
141
|
4 |
|
if (isset($fieldMapping['targetDocument'])) { |
142
|
4 |
|
$possibleEmbeds = [$fieldMapping['targetDocument']]; |
143
|
2 |
|
} elseif (isset($fieldMapping['discriminatorMap'])) { |
144
|
1 |
|
$possibleEmbeds = array_unique($fieldMapping['discriminatorMap']); |
145
|
|
|
} else { |
146
|
1 |
|
continue; |
147
|
|
|
} |
148
|
|
|
|
149
|
4 |
|
foreach ($possibleEmbeds as $embed) { |
150
|
4 |
|
if (isset($embeddedDocumentIndexes[$embed])) { |
151
|
2 |
|
$embeddedIndexes = $embeddedDocumentIndexes[$embed]; |
152
|
|
|
} else { |
153
|
4 |
|
$embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited); |
154
|
4 |
|
$embeddedDocumentIndexes[$embed] = $embeddedIndexes; |
155
|
|
|
} |
156
|
|
|
|
157
|
4 |
|
foreach ($embeddedIndexes as $embeddedIndex) { |
158
|
2 |
|
foreach ($embeddedIndex['keys'] as $key => $value) { |
159
|
2 |
|
$embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value; |
160
|
2 |
|
unset($embeddedIndex['keys'][$key]); |
161
|
|
|
} |
162
|
|
|
|
163
|
4 |
|
$indexes[] = $embeddedIndex; |
164
|
|
|
} |
165
|
|
|
} |
166
|
23 |
|
} elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) { |
167
|
11 |
|
foreach ($indexes as $idx => $index) { |
168
|
10 |
|
$newKeys = []; |
169
|
10 |
|
foreach ($index['keys'] as $key => $v) { |
170
|
10 |
|
if ($key === $fieldMapping['name']) { |
171
|
2 |
|
$key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key); |
172
|
|
|
} |
173
|
|
|
|
174
|
10 |
|
$newKeys[$key] = $v; |
175
|
|
|
} |
176
|
|
|
|
177
|
23 |
|
$indexes[$idx]['keys'] = $newKeys; |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
23 |
|
return $indexes; |
183
|
|
|
} |
184
|
|
|
|
185
|
23 |
|
private function prepareIndexes(ClassMetadata $class): array |
186
|
|
|
{ |
187
|
23 |
|
$persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name); |
188
|
23 |
|
$indexes = $class->getIndexes(); |
189
|
23 |
|
$newIndexes = []; |
190
|
|
|
|
191
|
23 |
|
foreach ($indexes as $index) { |
192
|
|
|
$newIndex = [ |
193
|
22 |
|
'keys' => [], |
194
|
22 |
|
'options' => $index['options'], |
195
|
|
|
]; |
196
|
|
|
|
197
|
22 |
|
foreach ($index['keys'] as $key => $value) { |
198
|
22 |
|
$key = $persister->prepareFieldName($key); |
199
|
22 |
|
if ($class->hasField($key)) { |
200
|
17 |
|
$mapping = $class->getFieldMapping($key); |
201
|
17 |
|
$newIndex['keys'][$mapping['name']] = $value; |
202
|
|
|
} else { |
203
|
22 |
|
$newIndex['keys'][$key] = $value; |
204
|
|
|
} |
205
|
|
|
} |
206
|
|
|
|
207
|
22 |
|
$newIndexes[] = $newIndex; |
208
|
|
|
} |
209
|
|
|
|
210
|
23 |
|
return $newIndexes; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Ensure the given document's indexes are created. |
215
|
|
|
* |
216
|
|
|
* @throws \InvalidArgumentException |
217
|
|
|
*/ |
218
|
19 |
|
public function ensureDocumentIndexes(string $documentName, ?int $timeoutMs = null): void |
219
|
|
|
{ |
220
|
19 |
|
$class = $this->dm->getClassMetadata($documentName); |
221
|
19 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
222
|
|
|
throw new \InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.'); |
223
|
|
|
} |
224
|
|
|
|
225
|
19 |
|
if ($class->isFile) { |
226
|
2 |
|
$this->ensureGridFSIndexes($class); |
227
|
|
|
} |
228
|
|
|
|
229
|
19 |
|
$indexes = $this->getDocumentIndexes($documentName); |
230
|
19 |
|
if (! $indexes) { |
|
|
|
|
231
|
4 |
|
return; |
232
|
|
|
} |
233
|
|
|
|
234
|
18 |
|
$collection = $this->dm->getDocumentCollection($class->name); |
235
|
18 |
|
foreach ($indexes as $index) { |
236
|
18 |
|
$keys = $index['keys']; |
237
|
18 |
|
$options = $index['options']; |
238
|
|
|
|
239
|
18 |
|
if (! isset($options['timeout']) && isset($timeoutMs)) { |
240
|
1 |
|
$options['timeout'] = $timeoutMs; |
241
|
|
|
} |
242
|
|
|
|
243
|
18 |
|
$collection->createIndex($keys, $options); |
244
|
|
|
} |
245
|
18 |
|
} |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* Delete indexes for all documents that can be loaded with the |
249
|
|
|
* metadata factory. |
250
|
|
|
*/ |
251
|
1 |
|
public function deleteIndexes(): void |
252
|
|
|
{ |
253
|
1 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
254
|
1 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
255
|
1 |
|
continue; |
256
|
|
|
} |
257
|
|
|
|
258
|
1 |
|
$this->deleteDocumentIndexes($class->name); |
259
|
|
|
} |
260
|
1 |
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Delete the given document's indexes. |
264
|
|
|
* |
265
|
|
|
* @throws \InvalidArgumentException |
266
|
|
|
*/ |
267
|
2 |
|
public function deleteDocumentIndexes(string $documentName): void |
268
|
|
|
{ |
269
|
2 |
|
$class = $this->dm->getClassMetadata($documentName); |
270
|
2 |
|
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
|
2 |
|
$this->dm->getDocumentCollection($documentName)->dropIndexes(); |
275
|
2 |
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Create all the mapped document collections in the metadata factory. |
279
|
|
|
*/ |
280
|
1 |
|
public function createCollections(): void |
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->createDocumentCollection($class->name); |
287
|
|
|
} |
288
|
1 |
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Create the document collection for a mapped class. |
292
|
|
|
* |
293
|
|
|
* @throws \InvalidArgumentException |
294
|
|
|
*/ |
295
|
5 |
|
public function createDocumentCollection(string $documentName): void |
296
|
|
|
{ |
297
|
5 |
|
$class = $this->dm->getClassMetadata($documentName); |
298
|
|
|
|
299
|
5 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
300
|
|
|
throw new \InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.'); |
301
|
|
|
} |
302
|
|
|
|
303
|
5 |
|
if ($class->isFile) { |
304
|
2 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files'); |
305
|
2 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks'); |
306
|
|
|
|
307
|
2 |
|
return; |
308
|
|
|
} |
309
|
|
|
|
310
|
4 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection( |
311
|
4 |
|
$class->getCollection(), |
312
|
|
|
[ |
313
|
4 |
|
'capped' => $class->getCollectionCapped(), |
314
|
4 |
|
'size' => $class->getCollectionSize(), |
315
|
4 |
|
'max' => $class->getCollectionMax(), |
316
|
|
|
] |
317
|
|
|
); |
318
|
4 |
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Drop all the mapped document collections in the metadata factory. |
322
|
|
|
*/ |
323
|
1 |
|
public function dropCollections(): void |
324
|
|
|
{ |
325
|
1 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
326
|
1 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
327
|
1 |
|
continue; |
328
|
|
|
} |
329
|
|
|
|
330
|
1 |
|
$this->dropDocumentCollection($class->name); |
331
|
|
|
} |
332
|
1 |
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Drop the document collection for a mapped class. |
336
|
|
|
* |
337
|
|
|
* @throws \InvalidArgumentException |
338
|
|
|
*/ |
339
|
5 |
|
public function dropDocumentCollection(string $documentName): void |
340
|
|
|
{ |
341
|
5 |
|
$class = $this->dm->getClassMetadata($documentName); |
342
|
5 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
343
|
|
|
throw new \InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.'); |
344
|
|
|
} |
345
|
|
|
|
346
|
5 |
|
$this->dm->getDocumentCollection($documentName)->drop(); |
347
|
|
|
|
348
|
5 |
|
if (! $class->isFile) { |
349
|
4 |
|
return; |
350
|
|
|
} |
351
|
|
|
|
352
|
2 |
|
$this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop(); |
353
|
2 |
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Drop all the mapped document databases in the metadata factory. |
357
|
|
|
*/ |
358
|
1 |
|
public function dropDatabases(): void |
359
|
|
|
{ |
360
|
1 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
361
|
1 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
362
|
1 |
|
continue; |
363
|
|
|
} |
364
|
|
|
|
365
|
1 |
|
$this->dropDocumentDatabase($class->name); |
366
|
|
|
} |
367
|
1 |
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Drop the document database for a mapped class. |
371
|
|
|
* |
372
|
|
|
* @throws \InvalidArgumentException |
373
|
|
|
*/ |
374
|
2 |
|
public function dropDocumentDatabase(string $documentName): void |
375
|
|
|
{ |
376
|
2 |
|
$class = $this->dm->getClassMetadata($documentName); |
377
|
2 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
378
|
|
|
throw new \InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.'); |
379
|
|
|
} |
380
|
|
|
|
381
|
2 |
|
$this->dm->getDocumentDatabase($documentName)->drop(); |
382
|
2 |
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Determine if an index returned by MongoCollection::getIndexInfo() can be |
386
|
|
|
* considered equivalent to an index in class metadata. |
387
|
|
|
* |
388
|
|
|
* Indexes are considered different if: |
389
|
|
|
* |
390
|
|
|
* (a) Key/direction pairs differ or are not in the same order |
391
|
|
|
* (b) Sparse or unique options differ |
392
|
|
|
* (c) Mongo index is unique without dropDups and mapped index is unique |
393
|
|
|
* with dropDups |
394
|
|
|
* (d) Geospatial options differ (bits, max, min) |
395
|
|
|
* (e) The partialFilterExpression differs |
396
|
|
|
* |
397
|
|
|
* Regarding (c), the inverse case is not a reason to delete and |
398
|
|
|
* recreate the index, since dropDups only affects creation of |
399
|
|
|
* the unique index. Additionally, the background option is only |
400
|
|
|
* relevant to index creation and is not considered. |
401
|
|
|
* |
402
|
|
|
* @param array|IndexInfo $mongoIndex Mongo index data. |
403
|
|
|
*/ |
404
|
47 |
|
public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, array $documentIndex): bool |
405
|
|
|
{ |
406
|
47 |
|
$documentIndexOptions = $documentIndex['options']; |
407
|
|
|
|
408
|
47 |
|
if (! $this->isEquivalentIndexKeys($mongoIndex, $documentIndex)) { |
409
|
4 |
|
return false; |
410
|
|
|
} |
411
|
|
|
|
412
|
43 |
|
if (empty($mongoIndex['sparse']) xor empty($documentIndexOptions['sparse'])) { |
413
|
2 |
|
return false; |
414
|
|
|
} |
415
|
|
|
|
416
|
41 |
|
if (empty($mongoIndex['unique']) xor empty($documentIndexOptions['unique'])) { |
417
|
2 |
|
return false; |
418
|
|
|
} |
419
|
|
|
|
420
|
39 |
|
if (! empty($mongoIndex['unique']) && empty($mongoIndex['dropDups']) && |
421
|
39 |
|
! empty($documentIndexOptions['unique']) && ! empty($documentIndexOptions['dropDups'])) { |
422
|
1 |
|
return false; |
423
|
|
|
} |
424
|
|
|
|
425
|
38 |
|
foreach (['bits', 'max', 'min'] as $option) { |
426
|
38 |
|
if (isset($mongoIndex[$option]) xor isset($documentIndexOptions[$option])) { |
427
|
6 |
|
return false; |
428
|
|
|
} |
429
|
|
|
|
430
|
36 |
|
if (isset($mongoIndex[$option], $documentIndexOptions[$option]) && |
431
|
36 |
|
$mongoIndex[$option] !== $documentIndexOptions[$option]) { |
432
|
36 |
|
return false; |
433
|
|
|
} |
434
|
|
|
} |
435
|
|
|
|
436
|
29 |
|
if (empty($mongoIndex['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) { |
437
|
2 |
|
return false; |
438
|
|
|
} |
439
|
|
|
|
440
|
27 |
|
if (isset($mongoIndex['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) && |
441
|
27 |
|
$mongoIndex['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) { |
442
|
1 |
|
return false; |
443
|
|
|
} |
444
|
|
|
|
445
|
26 |
|
if (isset($mongoIndex['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) { |
446
|
2 |
|
return false; |
447
|
|
|
} |
448
|
|
|
|
449
|
24 |
|
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
|
24 |
|
if (isset($mongoIndex[$option], $documentIndexOptions[$option]) && |
453
|
24 |
|
$mongoIndex[$option] !== $documentIndexOptions[$option]) { |
454
|
24 |
|
return false; |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
458
|
21 |
|
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
|
47 |
|
private function isEquivalentIndexKeys($mongoIndex, array $documentIndex): bool |
468
|
|
|
{ |
469
|
47 |
|
$mongoIndexKeys = $mongoIndex['key']; |
470
|
47 |
|
$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
|
47 |
|
if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') { |
478
|
15 |
|
unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']); |
479
|
|
|
|
480
|
|
|
$documentIndexKeys = array_filter($documentIndexKeys, 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
|
47 |
|
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(): void |
531
|
|
|
{ |
532
|
|
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
533
|
|
|
if ($class->isMappedSuperclass || ! $class->isSharded()) { |
534
|
|
|
continue; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
$this->ensureDocumentSharding($class->name); |
538
|
|
|
} |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* Ensure sharding for collection by document name. |
543
|
|
|
* |
544
|
|
|
* @throws MongoDBException |
545
|
|
|
*/ |
546
|
5 |
|
public function ensureDocumentSharding(string $documentName): void |
547
|
|
|
{ |
548
|
5 |
|
$class = $this->dm->getClassMetadata($documentName); |
549
|
5 |
|
if (! $class->isSharded()) { |
550
|
|
|
return; |
551
|
|
|
} |
552
|
|
|
|
553
|
5 |
|
if ($this->collectionIsSharded($documentName)) { |
554
|
1 |
|
return; |
555
|
|
|
} |
556
|
|
|
|
557
|
5 |
|
$this->enableShardingForDbByDocumentName($documentName); |
558
|
|
|
|
559
|
|
|
try { |
560
|
5 |
|
$this->runShardCollectionCommand($documentName); |
561
|
1 |
|
} catch (RuntimeException $e) { |
|
|
|
|
562
|
1 |
|
throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage()); |
563
|
|
|
} |
564
|
4 |
|
} |
565
|
|
|
|
566
|
|
|
/** |
567
|
|
|
* Enable sharding for database which contains documents with given name. |
568
|
|
|
* |
569
|
|
|
* @throws MongoDBException |
570
|
|
|
*/ |
571
|
5 |
|
public function enableShardingForDbByDocumentName(string $documentName): void |
572
|
|
|
{ |
573
|
5 |
|
$dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName(); |
574
|
5 |
|
$adminDb = $this->dm->getClient()->selectDatabase('admin'); |
575
|
|
|
|
576
|
|
|
try { |
577
|
5 |
|
$adminDb->command(['enableSharding' => $dbName]); |
578
|
4 |
|
} catch (ServerException $e) { |
|
|
|
|
579
|
|
|
// Don't throw an exception if sharding is already enabled; there's just no other way to check this |
580
|
4 |
|
if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) { |
581
|
4 |
|
throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage()); |
582
|
|
|
} |
583
|
|
|
} catch (RuntimeException $e) { |
|
|
|
|
584
|
|
|
throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage()); |
585
|
|
|
} |
586
|
5 |
|
} |
587
|
|
|
|
588
|
5 |
|
private function runShardCollectionCommand(string $documentName): array |
589
|
|
|
{ |
590
|
5 |
|
$class = $this->dm->getClassMetadata($documentName); |
591
|
5 |
|
$dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName(); |
592
|
5 |
|
$shardKey = $class->getShardKey(); |
593
|
5 |
|
$adminDb = $this->dm->getClient()->selectDatabase('admin'); |
594
|
|
|
|
595
|
5 |
|
$result = $adminDb->command( |
596
|
|
|
[ |
597
|
5 |
|
'shardCollection' => $dbName . '.' . $class->getCollection(), |
598
|
5 |
|
'key' => $shardKey['keys'], |
599
|
|
|
] |
600
|
4 |
|
)->toArray()[0]; |
601
|
|
|
|
602
|
4 |
|
return $result; |
603
|
|
|
} |
604
|
|
|
|
605
|
2 |
|
private function ensureGridFSIndexes(ClassMetadata $class): void |
606
|
|
|
{ |
607
|
2 |
|
$this->ensureChunksIndex($class); |
608
|
2 |
|
$this->ensureFilesIndex($class); |
609
|
2 |
|
} |
610
|
|
|
|
611
|
2 |
|
private function ensureChunksIndex(ClassMetadata $class): void |
612
|
|
|
{ |
613
|
2 |
|
$chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection(); |
|
|
|
|
614
|
2 |
|
foreach ($chunksCollection->listIndexes() as $index) { |
615
|
|
|
if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) { |
616
|
|
|
return; |
617
|
|
|
} |
618
|
|
|
} |
619
|
|
|
|
620
|
2 |
|
$chunksCollection->createIndex(self::GRIDFS_FILE_COLLECTION_INDEX, ['unique' => true]); |
621
|
2 |
|
} |
622
|
|
|
|
623
|
2 |
|
private function ensureFilesIndex(ClassMetadata $class): void |
624
|
|
|
{ |
625
|
2 |
|
$filesCollection = $this->dm->getDocumentCollection($class->getName()); |
|
|
|
|
626
|
2 |
|
foreach ($filesCollection->listIndexes() as $index) { |
627
|
|
|
if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) { |
628
|
|
|
return; |
629
|
|
|
} |
630
|
|
|
} |
631
|
|
|
|
632
|
2 |
|
$filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX); |
633
|
2 |
|
} |
634
|
|
|
|
635
|
5 |
|
private function collectionIsSharded(string $documentName): bool |
636
|
|
|
{ |
637
|
5 |
|
$class = $this->dm->getClassMetadata($documentName); |
638
|
|
|
|
639
|
5 |
|
$database = $this->dm->getDocumentDatabase($documentName); |
640
|
5 |
|
$collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]); |
641
|
5 |
|
if (! iterator_count($collections)) { |
642
|
1 |
|
return false; |
643
|
|
|
} |
644
|
|
|
|
645
|
4 |
|
$stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0]; |
646
|
4 |
|
return (bool) ($stats['sharded'] ?? false); |
647
|
|
|
} |
648
|
|
|
} |
649
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.