1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Doctrine\ODM\MongoDB; |
6
|
|
|
|
7
|
|
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; |
8
|
|
|
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; |
9
|
|
|
use Doctrine\ODM\MongoDB\Repository\ViewRepository; |
10
|
|
|
use InvalidArgumentException; |
11
|
|
|
use MongoDB\Driver\Exception\RuntimeException; |
12
|
|
|
use MongoDB\Driver\Exception\ServerException; |
13
|
|
|
use MongoDB\Driver\WriteConcern; |
14
|
|
|
use MongoDB\Model\IndexInfo; |
15
|
|
|
use function array_diff_key; |
16
|
|
|
use function array_filter; |
17
|
|
|
use function array_merge; |
18
|
|
|
use function array_unique; |
19
|
|
|
use function assert; |
20
|
|
|
use function is_string; |
21
|
|
|
use function iterator_count; |
22
|
|
|
use function iterator_to_array; |
23
|
|
|
use function ksort; |
24
|
|
|
use function sprintf; |
25
|
|
|
|
26
|
|
|
final class SchemaManager |
27
|
|
|
{ |
28
|
|
|
private const GRIDFS_FILE_COLLECTION_INDEX = ['files_id' => 1, 'n' => 1]; |
29
|
|
|
|
30
|
|
|
private const GRIDFS_CHUNKS_COLLECTION_INDEX = ['filename' => 1, 'uploadDate' => 1]; |
31
|
|
|
|
32
|
|
|
private const CODE_SHARDING_ALREADY_INITIALIZED = 23; |
33
|
|
|
|
34
|
|
|
private const ALLOWED_MISSING_INDEX_OPTIONS = [ |
35
|
|
|
'background', |
36
|
|
|
'partialFilterExpression', |
37
|
|
|
'sparse', |
38
|
|
|
'unique', |
39
|
|
|
'weights', |
40
|
|
|
'default_language', |
41
|
|
|
'language_override', |
42
|
|
|
'textIndexVersion', |
43
|
|
|
'name', |
44
|
|
|
]; |
45
|
|
|
|
46
|
|
|
/** @var DocumentManager */ |
47
|
|
|
protected $dm; |
48
|
|
|
|
49
|
|
|
/** @var ClassMetadataFactory */ |
50
|
|
|
protected $metadataFactory; |
51
|
|
|
|
52
|
1832 |
|
public function __construct(DocumentManager $dm, ClassMetadataFactory $cmf) |
53
|
|
|
{ |
54
|
1832 |
|
$this->dm = $dm; |
55
|
1832 |
|
$this->metadataFactory = $cmf; |
56
|
1832 |
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Ensure indexes are created for all documents that can be loaded with the |
60
|
|
|
* metadata factory. |
61
|
|
|
*/ |
62
|
5 |
|
public function ensureIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void |
63
|
|
|
{ |
64
|
5 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
65
|
5 |
|
assert($class instanceof ClassMetadata); |
66
|
5 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
67
|
5 |
|
continue; |
68
|
|
|
} |
69
|
|
|
|
70
|
5 |
|
$this->ensureDocumentIndexes($class->name, $maxTimeMs, $writeConcern, $background); |
71
|
|
|
} |
72
|
5 |
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Ensure indexes exist for all mapped document classes. |
76
|
|
|
* |
77
|
|
|
* Indexes that exist in MongoDB but not the document metadata will be |
78
|
|
|
* deleted. |
79
|
|
|
*/ |
80
|
|
|
public function updateIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
81
|
|
|
{ |
82
|
|
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
83
|
|
|
assert($class instanceof ClassMetadata); |
84
|
|
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
85
|
|
|
continue; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
$this->updateDocumentIndexes($class->name, $maxTimeMs, $writeConcern); |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Ensure indexes exist for the mapped document class. |
94
|
|
|
* |
95
|
|
|
* Indexes that exist in MongoDB but not the document metadata will be |
96
|
|
|
* deleted. |
97
|
|
|
* |
98
|
|
|
* @throws InvalidArgumentException |
99
|
|
|
*/ |
100
|
9 |
|
public function updateDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
101
|
|
|
{ |
102
|
9 |
|
$class = $this->dm->getClassMetadata($documentName); |
103
|
|
|
|
104
|
9 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
105
|
|
|
throw new InvalidArgumentException('Cannot update document indexes for mapped super classes, embedded documents or aggregation result documents.'); |
106
|
|
|
} |
107
|
|
|
|
108
|
9 |
|
$documentIndexes = $this->getDocumentIndexes($documentName); |
109
|
9 |
|
$collection = $this->dm->getDocumentCollection($documentName); |
110
|
9 |
|
$mongoIndexes = iterator_to_array($collection->listIndexes()); |
111
|
|
|
|
112
|
|
|
/* Determine which Mongo indexes should be deleted. Exclude the ID index |
113
|
|
|
* and those that are equivalent to any in the class metadata. |
114
|
|
|
*/ |
115
|
9 |
|
$self = $this; |
116
|
|
|
$mongoIndexes = array_filter($mongoIndexes, static function (IndexInfo $mongoIndex) use ($documentIndexes, $self) { |
117
|
4 |
|
if ($mongoIndex['name'] === '_id_') { |
118
|
|
|
return false; |
119
|
|
|
} |
120
|
|
|
|
121
|
4 |
|
foreach ($documentIndexes as $documentIndex) { |
122
|
4 |
|
if ($self->isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentIndex)) { |
123
|
|
|
return false; |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
4 |
|
return true; |
128
|
9 |
|
}); |
129
|
|
|
|
130
|
|
|
// Delete indexes that do not exist in class metadata |
131
|
9 |
|
foreach ($mongoIndexes as $mongoIndex) { |
132
|
4 |
|
if (! isset($mongoIndex['name'])) { |
133
|
|
|
continue; |
134
|
|
|
} |
135
|
|
|
|
136
|
4 |
|
$collection->dropIndex($mongoIndex['name'], $this->getWriteOptions($maxTimeMs, $writeConcern)); |
137
|
|
|
} |
138
|
|
|
|
139
|
9 |
|
$this->ensureDocumentIndexes($documentName, $maxTimeMs, $writeConcern); |
140
|
9 |
|
} |
141
|
|
|
|
142
|
47 |
|
public function getDocumentIndexes(string $documentName) : array |
143
|
|
|
{ |
144
|
47 |
|
$visited = []; |
145
|
|
|
|
146
|
47 |
|
return $this->doGetDocumentIndexes($documentName, $visited); |
147
|
|
|
} |
148
|
|
|
|
149
|
47 |
|
private function doGetDocumentIndexes(string $documentName, array &$visited) : array |
150
|
|
|
{ |
151
|
47 |
|
if (isset($visited[$documentName])) { |
152
|
5 |
|
return []; |
153
|
|
|
} |
154
|
|
|
|
155
|
47 |
|
$visited[$documentName] = true; |
156
|
|
|
|
157
|
47 |
|
$class = $this->dm->getClassMetadata($documentName); |
158
|
47 |
|
$indexes = $this->prepareIndexes($class); |
159
|
47 |
|
$embeddedDocumentIndexes = []; |
160
|
|
|
|
161
|
|
|
// Add indexes from embedded & referenced documents |
162
|
47 |
|
foreach ($class->fieldMappings as $fieldMapping) { |
163
|
47 |
|
if (isset($fieldMapping['embedded'])) { |
164
|
14 |
|
if (isset($fieldMapping['targetDocument'])) { |
165
|
14 |
|
$possibleEmbeds = [$fieldMapping['targetDocument']]; |
166
|
6 |
|
} elseif (isset($fieldMapping['discriminatorMap'])) { |
167
|
1 |
|
$possibleEmbeds = array_unique($fieldMapping['discriminatorMap']); |
168
|
|
|
} else { |
169
|
5 |
|
continue; |
170
|
|
|
} |
171
|
|
|
|
172
|
14 |
|
foreach ($possibleEmbeds as $embed) { |
173
|
14 |
|
if (isset($embeddedDocumentIndexes[$embed])) { |
174
|
7 |
|
$embeddedIndexes = $embeddedDocumentIndexes[$embed]; |
175
|
|
|
} else { |
176
|
14 |
|
$embeddedIndexes = $this->doGetDocumentIndexes($embed, $visited); |
177
|
14 |
|
$embeddedDocumentIndexes[$embed] = $embeddedIndexes; |
178
|
|
|
} |
179
|
|
|
|
180
|
14 |
|
foreach ($embeddedIndexes as $embeddedIndex) { |
181
|
4 |
|
foreach ($embeddedIndex['keys'] as $key => $value) { |
182
|
4 |
|
$embeddedIndex['keys'][$fieldMapping['name'] . '.' . $key] = $value; |
183
|
4 |
|
unset($embeddedIndex['keys'][$key]); |
184
|
|
|
} |
185
|
|
|
|
186
|
4 |
|
if (isset($embeddedIndex['options']['name'])) { |
187
|
2 |
|
$embeddedIndex['options']['name'] = sprintf('%s_%s', $fieldMapping['name'], $embeddedIndex['options']['name']); |
188
|
|
|
} |
189
|
|
|
|
190
|
4 |
|
$indexes[] = $embeddedIndex; |
191
|
|
|
} |
192
|
|
|
} |
193
|
47 |
|
} elseif (isset($fieldMapping['reference']) && isset($fieldMapping['targetDocument'])) { |
194
|
29 |
|
foreach ($indexes as $idx => $index) { |
195
|
23 |
|
$newKeys = []; |
196
|
23 |
|
foreach ($index['keys'] as $key => $v) { |
197
|
23 |
|
if ($key === $fieldMapping['name']) { |
198
|
6 |
|
$key = ClassMetadata::getReferenceFieldName($fieldMapping['storeAs'], $key); |
199
|
|
|
} |
200
|
|
|
|
201
|
23 |
|
$newKeys[$key] = $v; |
202
|
|
|
} |
203
|
|
|
|
204
|
23 |
|
$indexes[$idx]['keys'] = $newKeys; |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
47 |
|
return $indexes; |
210
|
|
|
} |
211
|
|
|
|
212
|
47 |
|
private function prepareIndexes(ClassMetadata $class) : array |
213
|
|
|
{ |
214
|
47 |
|
$persister = $this->dm->getUnitOfWork()->getDocumentPersister($class->name); |
215
|
47 |
|
$indexes = $class->getIndexes(); |
216
|
47 |
|
$newIndexes = []; |
217
|
|
|
|
218
|
47 |
|
foreach ($indexes as $index) { |
219
|
|
|
$newIndex = [ |
220
|
41 |
|
'keys' => [], |
221
|
41 |
|
'options' => $index['options'], |
222
|
|
|
]; |
223
|
|
|
|
224
|
41 |
|
foreach ($index['keys'] as $key => $value) { |
225
|
41 |
|
$key = $persister->prepareFieldName($key); |
226
|
41 |
|
if ($class->hasField($key)) { |
227
|
36 |
|
$mapping = $class->getFieldMapping($key); |
228
|
36 |
|
$newIndex['keys'][$mapping['name']] = $value; |
229
|
|
|
} else { |
230
|
12 |
|
$newIndex['keys'][$key] = $value; |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
41 |
|
$newIndexes[] = $newIndex; |
235
|
|
|
} |
236
|
|
|
|
237
|
47 |
|
return $newIndexes; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Ensure the given document's indexes are created. |
242
|
|
|
* |
243
|
|
|
* @throws InvalidArgumentException |
244
|
|
|
*/ |
245
|
43 |
|
public function ensureDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void |
246
|
|
|
{ |
247
|
43 |
|
$class = $this->dm->getClassMetadata($documentName); |
248
|
43 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
249
|
|
|
throw new InvalidArgumentException('Cannot create document indexes for mapped super classes, embedded documents or query result documents.'); |
250
|
|
|
} |
251
|
|
|
|
252
|
43 |
|
if ($class->isFile) { |
253
|
10 |
|
$this->ensureGridFSIndexes($class, $maxTimeMs, $writeConcern, $background); |
254
|
|
|
} |
255
|
|
|
|
256
|
43 |
|
$indexes = $this->getDocumentIndexes($documentName); |
257
|
43 |
|
if (! $indexes) { |
|
|
|
|
258
|
13 |
|
return; |
259
|
|
|
} |
260
|
|
|
|
261
|
37 |
|
$collection = $this->dm->getDocumentCollection($class->name); |
262
|
37 |
|
foreach ($indexes as $index) { |
263
|
37 |
|
$collection->createIndex($index['keys'], $this->getWriteOptions($maxTimeMs, $writeConcern, $index['options'] + ['background' => $background])); |
264
|
|
|
} |
265
|
36 |
|
} |
266
|
|
|
|
267
|
|
|
/** |
268
|
|
|
* Delete indexes for all documents that can be loaded with the |
269
|
|
|
* metadata factory. |
270
|
|
|
*/ |
271
|
4 |
|
public function deleteIndexes(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
272
|
|
|
{ |
273
|
4 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
274
|
4 |
|
assert($class instanceof ClassMetadata); |
275
|
4 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
276
|
4 |
|
continue; |
277
|
|
|
} |
278
|
|
|
|
279
|
4 |
|
$this->deleteDocumentIndexes($class->name, $maxTimeMs, $writeConcern); |
280
|
|
|
} |
281
|
4 |
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* Delete the given document's indexes. |
285
|
|
|
* |
286
|
|
|
* @throws InvalidArgumentException |
287
|
|
|
*/ |
288
|
8 |
|
public function deleteDocumentIndexes(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
289
|
|
|
{ |
290
|
8 |
|
$class = $this->dm->getClassMetadata($documentName); |
291
|
8 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument || $class->isView()) { |
292
|
|
|
throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.'); |
293
|
|
|
} |
294
|
|
|
|
295
|
8 |
|
$this->dm->getDocumentCollection($documentName)->dropIndexes($this->getWriteOptions($maxTimeMs, $writeConcern)); |
296
|
8 |
|
} |
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* Create all the mapped document collections in the metadata factory. |
300
|
|
|
*/ |
301
|
4 |
|
public function createCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
302
|
|
|
{ |
303
|
4 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
304
|
4 |
|
assert($class instanceof ClassMetadata); |
305
|
4 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
306
|
4 |
|
continue; |
307
|
|
|
} |
308
|
4 |
|
$this->createDocumentCollection($class->name, $maxTimeMs, $writeConcern); |
309
|
|
|
} |
310
|
4 |
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Create the document collection for a mapped class. |
314
|
|
|
* |
315
|
|
|
* @throws InvalidArgumentException |
316
|
|
|
*/ |
317
|
21 |
|
public function createDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
318
|
|
|
{ |
319
|
21 |
|
$class = $this->dm->getClassMetadata($documentName); |
320
|
|
|
|
321
|
21 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
322
|
|
|
throw new InvalidArgumentException('Cannot create document collection for mapped super classes, embedded documents or query result documents.'); |
323
|
|
|
} |
324
|
|
|
|
325
|
21 |
|
if ($class->isView()) { |
326
|
11 |
|
$options = $this->getWriteOptions($maxTimeMs, $writeConcern); |
327
|
|
|
|
328
|
11 |
|
$rootClass = $class->getRootClass(); |
329
|
11 |
|
assert(is_string($rootClass)); |
330
|
|
|
|
331
|
11 |
|
$builder = $this->dm->createAggregationBuilder($rootClass); |
332
|
11 |
|
$repository = $this->dm->getRepository($class->name); |
333
|
11 |
|
assert($repository instanceof ViewRepository); |
334
|
11 |
|
$repository->createViewAggregation($builder); |
335
|
|
|
|
336
|
11 |
|
$collectionName = $this->dm->getDocumentCollection($rootClass)->getCollectionName(); |
337
|
11 |
|
$this->dm->getDocumentDatabase($documentName) |
338
|
11 |
|
->command([ |
339
|
11 |
|
'create' => $class->collection, |
340
|
11 |
|
'viewOn' => $collectionName, |
341
|
11 |
|
'pipeline' => $builder->getPipeline(), |
342
|
11 |
|
], $options); |
343
|
|
|
|
344
|
11 |
|
return; |
345
|
|
|
} |
346
|
|
|
|
347
|
14 |
|
if ($class->isFile) { |
348
|
8 |
|
$options = $this->getWriteOptions($maxTimeMs, $writeConcern); |
349
|
|
|
|
350
|
8 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.files', $options); |
351
|
8 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection($class->getBucketName() . '.chunks', $options); |
352
|
|
|
|
353
|
8 |
|
return; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
$options = [ |
357
|
10 |
|
'capped' => $class->getCollectionCapped(), |
358
|
10 |
|
'size' => $class->getCollectionSize(), |
359
|
10 |
|
'max' => $class->getCollectionMax(), |
360
|
|
|
]; |
361
|
|
|
|
362
|
10 |
|
$this->dm->getDocumentDatabase($documentName)->createCollection( |
363
|
10 |
|
$class->getCollection(), |
364
|
10 |
|
$this->getWriteOptions($maxTimeMs, $writeConcern, $options) |
365
|
|
|
); |
366
|
10 |
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Drop all the mapped document collections in the metadata factory. |
370
|
|
|
*/ |
371
|
4 |
|
public function dropCollections(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
372
|
|
|
{ |
373
|
4 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
374
|
4 |
|
assert($class instanceof ClassMetadata); |
375
|
4 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
376
|
4 |
|
continue; |
377
|
|
|
} |
378
|
|
|
|
379
|
4 |
|
$this->dropDocumentCollection($class->name, $maxTimeMs, $writeConcern); |
380
|
|
|
} |
381
|
4 |
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* Drop the document collection for a mapped class. |
385
|
|
|
* |
386
|
|
|
* @throws InvalidArgumentException |
387
|
|
|
*/ |
388
|
18 |
|
public function dropDocumentCollection(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
389
|
|
|
{ |
390
|
18 |
|
$class = $this->dm->getClassMetadata($documentName); |
391
|
18 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
392
|
|
|
throw new InvalidArgumentException('Cannot delete document indexes for mapped super classes, embedded documents or query result documents.'); |
393
|
|
|
} |
394
|
|
|
|
395
|
18 |
|
$options = $this->getWriteOptions($maxTimeMs, $writeConcern); |
396
|
|
|
|
397
|
18 |
|
$this->dm->getDocumentCollection($documentName)->drop($options); |
398
|
|
|
|
399
|
18 |
|
if (! $class->isFile) { |
400
|
14 |
|
return; |
401
|
|
|
} |
402
|
|
|
|
403
|
8 |
|
$this->dm->getDocumentBucket($documentName)->getChunksCollection()->drop($options); |
404
|
8 |
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* Drop all the mapped document databases in the metadata factory. |
408
|
|
|
*/ |
409
|
4 |
|
public function dropDatabases(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
410
|
|
|
{ |
411
|
4 |
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
412
|
4 |
|
assert($class instanceof ClassMetadata); |
413
|
4 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
414
|
4 |
|
continue; |
415
|
|
|
} |
416
|
|
|
|
417
|
4 |
|
$this->dropDocumentDatabase($class->name, $maxTimeMs, $writeConcern); |
418
|
|
|
} |
419
|
4 |
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* Drop the document database for a mapped class. |
423
|
|
|
* |
424
|
|
|
* @throws InvalidArgumentException |
425
|
|
|
*/ |
426
|
8 |
|
public function dropDocumentDatabase(string $documentName, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null) : void |
427
|
|
|
{ |
428
|
8 |
|
$class = $this->dm->getClassMetadata($documentName); |
429
|
8 |
|
if ($class->isMappedSuperclass || $class->isEmbeddedDocument || $class->isQueryResultDocument) { |
430
|
|
|
throw new InvalidArgumentException('Cannot drop document database for mapped super classes, embedded documents or query result documents.'); |
431
|
|
|
} |
432
|
|
|
|
433
|
8 |
|
$this->dm->getDocumentDatabase($documentName)->drop($this->getWriteOptions($maxTimeMs, $writeConcern)); |
434
|
8 |
|
} |
435
|
|
|
|
436
|
52 |
|
public function isMongoIndexEquivalentToDocumentIndex(IndexInfo $mongoIndex, array $documentIndex) : bool |
437
|
|
|
{ |
438
|
52 |
|
return $this->isEquivalentIndexKeys($mongoIndex, $documentIndex) && $this->isEquivalentIndexOptions($mongoIndex, $documentIndex); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Determine if the keys for a MongoDB index can be considered equivalent to |
443
|
|
|
* those for an index in class metadata. |
444
|
|
|
*/ |
445
|
52 |
|
private function isEquivalentIndexKeys(IndexInfo $mongoIndex, array $documentIndex) : bool |
446
|
|
|
{ |
447
|
52 |
|
$mongoIndexKeys = $mongoIndex['key']; |
448
|
52 |
|
$documentIndexKeys = $documentIndex['keys']; |
449
|
|
|
|
450
|
|
|
/* If we are dealing with text indexes, we need to unset internal fields |
451
|
|
|
* from the MongoDB index and filter out text fields from the document |
452
|
|
|
* index. This will leave only non-text fields, which we can compare as |
453
|
|
|
* normal. Any text fields in the document index will be compared later |
454
|
|
|
* with isEquivalentTextIndexWeights(). */ |
455
|
52 |
|
if (isset($mongoIndexKeys['_fts']) && $mongoIndexKeys['_fts'] === 'text') { |
456
|
15 |
|
unset($mongoIndexKeys['_fts'], $mongoIndexKeys['_ftsx']); |
457
|
|
|
|
458
|
|
|
$documentIndexKeys = array_filter($documentIndexKeys, static function ($type) { |
459
|
15 |
|
return $type !== 'text'; |
460
|
15 |
|
}); |
461
|
|
|
} |
462
|
|
|
|
463
|
|
|
/* Avoid a strict equality check here. The numeric type returned by |
464
|
|
|
* MongoDB may differ from the document index without implying that the |
465
|
|
|
* indexes themselves are inequivalent. */ |
466
|
|
|
// phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator |
467
|
52 |
|
return $mongoIndexKeys == $documentIndexKeys; |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
/** |
471
|
|
|
* Determine if an index returned by MongoCollection::getIndexInfo() can be |
472
|
|
|
* considered equivalent to an index in class metadata based on options. |
473
|
|
|
* |
474
|
|
|
* Indexes are considered different if: |
475
|
|
|
* |
476
|
|
|
* (a) Key/direction pairs differ or are not in the same order |
477
|
|
|
* (b) Sparse or unique options differ |
478
|
|
|
* (c) Geospatial options differ (bits, max, min) |
479
|
|
|
* (d) The partialFilterExpression differs |
480
|
|
|
* |
481
|
|
|
* The background option is only relevant to index creation and is not |
482
|
|
|
* considered. |
483
|
|
|
*/ |
484
|
45 |
|
private function isEquivalentIndexOptions(IndexInfo $mongoIndex, array $documentIndex) : bool |
485
|
|
|
{ |
486
|
45 |
|
$mongoIndexOptions = $mongoIndex->__debugInfo(); |
487
|
45 |
|
unset($mongoIndexOptions['v'], $mongoIndexOptions['ns'], $mongoIndexOptions['key']); |
488
|
|
|
|
489
|
45 |
|
$documentIndexOptions = $documentIndex['options']; |
490
|
|
|
|
491
|
45 |
|
if ($this->indexOptionsAreMissing($mongoIndexOptions, $documentIndexOptions)) { |
492
|
7 |
|
return false; |
493
|
|
|
} |
494
|
|
|
|
495
|
38 |
|
if (empty($mongoIndexOptions['sparse']) xor empty($documentIndexOptions['sparse'])) { |
496
|
2 |
|
return false; |
497
|
|
|
} |
498
|
|
|
|
499
|
36 |
|
if (empty($mongoIndexOptions['unique']) xor empty($documentIndexOptions['unique'])) { |
500
|
2 |
|
return false; |
501
|
|
|
} |
502
|
|
|
|
503
|
34 |
|
foreach (['bits', 'max', 'min'] as $option) { |
504
|
34 |
|
if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) && |
505
|
34 |
|
$mongoIndexOptions[$option] !== $documentIndexOptions[$option]) { |
506
|
3 |
|
return false; |
507
|
|
|
} |
508
|
|
|
} |
509
|
|
|
|
510
|
31 |
|
if (empty($mongoIndexOptions['partialFilterExpression']) xor empty($documentIndexOptions['partialFilterExpression'])) { |
511
|
2 |
|
return false; |
512
|
|
|
} |
513
|
|
|
|
514
|
29 |
|
if (isset($mongoIndexOptions['partialFilterExpression'], $documentIndexOptions['partialFilterExpression']) && |
515
|
29 |
|
$mongoIndexOptions['partialFilterExpression'] !== $documentIndexOptions['partialFilterExpression']) { |
516
|
1 |
|
return false; |
517
|
|
|
} |
518
|
|
|
|
519
|
28 |
|
if (isset($mongoIndexOptions['weights']) && ! $this->isEquivalentTextIndexWeights($mongoIndex, $documentIndex)) { |
520
|
2 |
|
return false; |
521
|
|
|
} |
522
|
|
|
|
523
|
26 |
|
foreach (['default_language', 'language_override', 'textIndexVersion'] as $option) { |
524
|
|
|
/* Text indexes will always report defaults for these options, so |
525
|
|
|
* only compare if we have explicit values in the document index. */ |
526
|
26 |
|
if (isset($mongoIndexOptions[$option], $documentIndexOptions[$option]) && |
527
|
26 |
|
$mongoIndexOptions[$option] !== $documentIndexOptions[$option]) { |
528
|
3 |
|
return false; |
529
|
|
|
} |
530
|
|
|
} |
531
|
|
|
|
532
|
23 |
|
return true; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
/** |
536
|
|
|
* Checks if any index options are missing. |
537
|
|
|
* |
538
|
|
|
* Options added to the ALLOWED_MISSING_INDEX_OPTIONS constant are ignored |
539
|
|
|
* and are expected to be checked later |
540
|
|
|
*/ |
541
|
45 |
|
private function indexOptionsAreMissing(array $mongoIndexOptions, array $documentIndexOptions) : bool |
542
|
|
|
{ |
543
|
45 |
|
foreach (self::ALLOWED_MISSING_INDEX_OPTIONS as $option) { |
544
|
45 |
|
unset($mongoIndexOptions[$option], $documentIndexOptions[$option]); |
545
|
|
|
} |
546
|
|
|
|
547
|
45 |
|
return array_diff_key($mongoIndexOptions, $documentIndexOptions) !== [] || array_diff_key($documentIndexOptions, $mongoIndexOptions) !== []; |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Determine if the text index weights for a MongoDB index can be considered |
552
|
|
|
* equivalent to those for an index in class metadata. |
553
|
|
|
*/ |
554
|
14 |
|
private function isEquivalentTextIndexWeights(IndexInfo $mongoIndex, array $documentIndex) : bool |
555
|
|
|
{ |
556
|
14 |
|
$mongoIndexWeights = $mongoIndex['weights']; |
557
|
14 |
|
$documentIndexWeights = $documentIndex['options']['weights'] ?? []; |
558
|
|
|
|
559
|
|
|
// If not specified, assign a default weight for text fields |
560
|
14 |
|
foreach ($documentIndex['keys'] as $key => $type) { |
561
|
14 |
|
if ($type !== 'text' || isset($documentIndexWeights[$key])) { |
562
|
5 |
|
continue; |
563
|
|
|
} |
564
|
|
|
|
565
|
9 |
|
$documentIndexWeights[$key] = 1; |
566
|
|
|
} |
567
|
|
|
|
568
|
|
|
/* MongoDB returns the weights sorted by field name, but we'll sort both |
569
|
|
|
* arrays in case that is internal behavior not to be relied upon. */ |
570
|
14 |
|
ksort($mongoIndexWeights); |
571
|
14 |
|
ksort($documentIndexWeights); |
572
|
|
|
|
573
|
|
|
/* Avoid a strict equality check here. The numeric type returned by |
574
|
|
|
* MongoDB may differ from the document index without implying that the |
575
|
|
|
* indexes themselves are inequivalent. */ |
576
|
|
|
// phpcs:disable SlevomatCodingStandard.Operators.DisallowEqualOperators.DisallowedEqualOperator |
577
|
14 |
|
return $mongoIndexWeights == $documentIndexWeights; |
578
|
|
|
} |
579
|
|
|
|
580
|
|
|
/** |
581
|
|
|
* Ensure collections are sharded for all documents that can be loaded with the |
582
|
|
|
* metadata factory. |
583
|
|
|
* |
584
|
|
|
* @throws MongoDBException |
585
|
|
|
*/ |
586
|
|
|
public function ensureSharding(?WriteConcern $writeConcern = null) : void |
587
|
|
|
{ |
588
|
|
|
foreach ($this->metadataFactory->getAllMetadata() as $class) { |
589
|
|
|
assert($class instanceof ClassMetadata); |
590
|
|
|
if ($class->isMappedSuperclass || ! $class->isSharded()) { |
591
|
|
|
continue; |
592
|
|
|
} |
593
|
|
|
|
594
|
|
|
$this->ensureDocumentSharding($class->name, $writeConcern); |
595
|
|
|
} |
596
|
|
|
} |
597
|
|
|
|
598
|
|
|
/** |
599
|
|
|
* Ensure sharding for collection by document name. |
600
|
|
|
* |
601
|
|
|
* @throws MongoDBException |
602
|
|
|
*/ |
603
|
12 |
|
public function ensureDocumentSharding(string $documentName, ?WriteConcern $writeConcern = null) : void |
604
|
|
|
{ |
605
|
12 |
|
$class = $this->dm->getClassMetadata($documentName); |
606
|
12 |
|
if (! $class->isSharded()) { |
607
|
|
|
return; |
608
|
|
|
} |
609
|
|
|
|
610
|
12 |
|
if ($this->collectionIsSharded($documentName)) { |
611
|
1 |
|
return; |
612
|
|
|
} |
613
|
|
|
|
614
|
12 |
|
$this->enableShardingForDbByDocumentName($documentName); |
615
|
|
|
|
616
|
|
|
try { |
617
|
12 |
|
$this->runShardCollectionCommand($documentName, $writeConcern); |
618
|
1 |
|
} catch (RuntimeException $e) { |
|
|
|
|
619
|
1 |
|
throw MongoDBException::failedToEnsureDocumentSharding($documentName, $e->getMessage()); |
620
|
|
|
} |
621
|
11 |
|
} |
622
|
|
|
|
623
|
|
|
/** |
624
|
|
|
* Enable sharding for database which contains documents with given name. |
625
|
|
|
* |
626
|
|
|
* @throws MongoDBException |
627
|
|
|
*/ |
628
|
12 |
|
public function enableShardingForDbByDocumentName(string $documentName) : void |
629
|
|
|
{ |
630
|
12 |
|
$dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName(); |
631
|
12 |
|
$adminDb = $this->dm->getClient()->selectDatabase('admin'); |
632
|
|
|
|
633
|
|
|
try { |
634
|
12 |
|
$adminDb->command(['enableSharding' => $dbName]); |
635
|
|
|
} catch (ServerException $e) { |
|
|
|
|
636
|
|
|
// Don't throw an exception if sharding is already enabled; there's just no other way to check this |
637
|
|
|
if ($e->getCode() !== self::CODE_SHARDING_ALREADY_INITIALIZED) { |
638
|
|
|
throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage()); |
639
|
|
|
} |
640
|
|
|
} catch (RuntimeException $e) { |
|
|
|
|
641
|
|
|
throw MongoDBException::failedToEnableSharding($dbName, $e->getMessage()); |
642
|
|
|
} |
643
|
12 |
|
} |
644
|
|
|
|
645
|
12 |
|
private function runShardCollectionCommand(string $documentName, ?WriteConcern $writeConcern = null) : array |
646
|
|
|
{ |
647
|
12 |
|
$class = $this->dm->getClassMetadata($documentName); |
648
|
12 |
|
$dbName = $this->dm->getDocumentDatabase($documentName)->getDatabaseName(); |
649
|
12 |
|
$shardKey = $class->getShardKey(); |
650
|
12 |
|
$adminDb = $this->dm->getClient()->selectDatabase('admin'); |
651
|
|
|
|
652
|
12 |
|
$shardKeyPart = []; |
653
|
12 |
|
foreach ($shardKey['keys'] as $key => $order) { |
654
|
12 |
|
if ($class->hasField($key)) { |
655
|
1 |
|
$mapping = $class->getFieldMapping($key); |
656
|
1 |
|
$fieldName = $mapping['name']; |
657
|
|
|
|
658
|
1 |
|
if ($class->isSingleValuedReference($key)) { |
659
|
1 |
|
$fieldName = ClassMetadata::getReferenceFieldName($mapping['storeAs'], $fieldName); |
660
|
|
|
} |
661
|
|
|
} else { |
662
|
11 |
|
$fieldName = $key; |
663
|
|
|
} |
664
|
|
|
|
665
|
12 |
|
$shardKeyPart[$fieldName] = $order; |
666
|
|
|
} |
667
|
|
|
|
668
|
12 |
|
return $adminDb->command( |
669
|
12 |
|
array_merge( |
670
|
|
|
[ |
671
|
12 |
|
'shardCollection' => $dbName . '.' . $class->getCollection(), |
672
|
12 |
|
'key' => $shardKeyPart, |
673
|
|
|
], |
674
|
12 |
|
$this->getWriteOptions(null, $writeConcern) |
675
|
|
|
) |
676
|
11 |
|
)->toArray()[0]; |
677
|
|
|
} |
678
|
|
|
|
679
|
10 |
|
private function ensureGridFSIndexes(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void |
680
|
|
|
{ |
681
|
10 |
|
$this->ensureChunksIndex($class, $maxTimeMs, $writeConcern, $background); |
682
|
10 |
|
$this->ensureFilesIndex($class, $maxTimeMs, $writeConcern, $background); |
683
|
10 |
|
} |
684
|
|
|
|
685
|
10 |
|
private function ensureChunksIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void |
686
|
|
|
{ |
687
|
10 |
|
$chunksCollection = $this->dm->getDocumentBucket($class->getName())->getChunksCollection(); |
|
|
|
|
688
|
10 |
|
foreach ($chunksCollection->listIndexes() as $index) { |
689
|
|
|
if ($index->isUnique() && $index->getKey() === self::GRIDFS_FILE_COLLECTION_INDEX) { |
690
|
|
|
return; |
691
|
|
|
} |
692
|
|
|
} |
693
|
|
|
|
694
|
10 |
|
$chunksCollection->createIndex( |
695
|
10 |
|
self::GRIDFS_FILE_COLLECTION_INDEX, |
696
|
10 |
|
$this->getWriteOptions($maxTimeMs, $writeConcern, ['unique' => true, 'background' => $background]) |
697
|
|
|
); |
698
|
10 |
|
} |
699
|
|
|
|
700
|
10 |
|
private function ensureFilesIndex(ClassMetadata $class, ?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, bool $background = false) : void |
701
|
|
|
{ |
702
|
10 |
|
$filesCollection = $this->dm->getDocumentCollection($class->getName()); |
|
|
|
|
703
|
10 |
|
foreach ($filesCollection->listIndexes() as $index) { |
704
|
|
|
if ($index->getKey() === self::GRIDFS_CHUNKS_COLLECTION_INDEX) { |
705
|
|
|
return; |
706
|
|
|
} |
707
|
|
|
} |
708
|
|
|
|
709
|
10 |
|
$filesCollection->createIndex(self::GRIDFS_CHUNKS_COLLECTION_INDEX, $this->getWriteOptions($maxTimeMs, $writeConcern, ['background' => $background])); |
710
|
10 |
|
} |
711
|
|
|
|
712
|
12 |
|
private function collectionIsSharded(string $documentName) : bool |
713
|
|
|
{ |
714
|
12 |
|
$class = $this->dm->getClassMetadata($documentName); |
715
|
|
|
|
716
|
12 |
|
$database = $this->dm->getDocumentDatabase($documentName); |
717
|
12 |
|
$collections = $database->listCollections(['filter' => ['name' => $class->getCollection()]]); |
718
|
12 |
|
if (! iterator_count($collections)) { |
719
|
8 |
|
return false; |
720
|
|
|
} |
721
|
|
|
|
722
|
4 |
|
$stats = $database->command(['collstats' => $class->getCollection()])->toArray()[0]; |
723
|
|
|
|
724
|
4 |
|
return (bool) ($stats['sharded'] ?? false); |
725
|
|
|
} |
726
|
|
|
|
727
|
104 |
|
private function getWriteOptions(?int $maxTimeMs = null, ?WriteConcern $writeConcern = null, array $options = []) : array |
728
|
|
|
{ |
729
|
104 |
|
unset($options['maxTimeMs'], $options['writeConcern']); |
730
|
|
|
|
731
|
104 |
|
if ($maxTimeMs !== null) { |
732
|
36 |
|
$options['maxTimeMs'] = $maxTimeMs; |
733
|
|
|
} |
734
|
|
|
|
735
|
104 |
|
if ($writeConcern !== null) { |
736
|
36 |
|
$options['writeConcern'] = $writeConcern; |
737
|
|
|
} |
738
|
|
|
|
739
|
104 |
|
return $options; |
740
|
|
|
} |
741
|
|
|
} |
742
|
|
|
|
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.