1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
namespace Spiral\ODM\Schemas; |
8
|
|
|
|
9
|
|
|
use Doctrine\Common\Inflector\Inflector; |
10
|
|
|
use Spiral\Models\AccessorInterface; |
11
|
|
|
use Spiral\Models\Reflections\ReflectionEntity; |
12
|
|
|
use Spiral\ODM\Configs\MutatorsConfig; |
13
|
|
|
use Spiral\ODM\Document; |
14
|
|
|
use Spiral\ODM\DocumentEntity; |
15
|
|
|
use Spiral\ODM\Entities\DocumentInstantiator; |
16
|
|
|
use Spiral\ODM\Exceptions\SchemaException; |
17
|
|
|
use Spiral\ODM\Schemas\Definitions\AggregationDefinition; |
18
|
|
|
use Spiral\ODM\Schemas\Definitions\CompositionDefinition; |
19
|
|
|
use Spiral\ODM\Schemas\Definitions\IndexDefinition; |
20
|
|
|
|
21
|
|
|
class DocumentSchema implements SchemaInterface |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* @var ReflectionEntity |
25
|
|
|
*/ |
26
|
|
|
private $reflection; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @invisible |
30
|
|
|
* |
31
|
|
|
* @var MutatorsConfig |
32
|
|
|
*/ |
33
|
|
|
private $mutators; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* @param ReflectionEntity $reflection |
37
|
|
|
* @param MutatorsConfig $config |
38
|
|
|
*/ |
39
|
|
|
public function __construct(ReflectionEntity $reflection, MutatorsConfig $config) |
40
|
|
|
{ |
41
|
|
|
$this->reflection = $reflection; |
42
|
|
|
$this->mutators = $config; |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* @return string |
47
|
|
|
*/ |
48
|
|
|
public function getClass(): string |
49
|
|
|
{ |
50
|
|
|
return $this->reflection->getName(); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @return ReflectionEntity |
55
|
|
|
*/ |
56
|
|
|
public function getReflection(): ReflectionEntity |
57
|
|
|
{ |
58
|
|
|
return $this->reflection; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @return string |
63
|
|
|
*/ |
64
|
|
|
public function getInstantiator(): string |
65
|
|
|
{ |
66
|
|
|
return $this->reflection->getConstant('INSTANTIATOR') ?? DocumentInstantiator::class; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* {@inheritdoc} |
71
|
|
|
*/ |
72
|
|
|
public function isEmbedded(): bool |
73
|
|
|
{ |
74
|
|
|
return !$this->reflection->isSubclassOf(Document::class) |
75
|
|
|
&& $this->reflection->isSubclassOf(DocumentEntity::class); |
76
|
|
|
} |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* {@inheritdoc} |
80
|
|
|
*/ |
81
|
|
|
public function getDatabase() |
82
|
|
|
{ |
83
|
|
|
if ($this->isEmbedded()) { |
84
|
|
|
throw new SchemaException( |
85
|
|
|
"Unable to get database name for embedded model {$this->reflection}" |
86
|
|
|
); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
$database = $this->reflection->getConstant('DATABASE'); |
90
|
|
|
if (empty($database)) { |
91
|
|
|
//Empty database to be used |
92
|
|
|
return null; |
93
|
|
|
} |
94
|
|
|
|
95
|
|
|
return $database; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* {@inheritdoc} |
100
|
|
|
*/ |
101
|
|
|
public function getCollection(): string |
102
|
|
|
{ |
103
|
|
|
if ($this->isEmbedded()) { |
104
|
|
|
throw new SchemaException( |
105
|
|
|
"Unable to get collection name for embedded model {$this->reflection}" |
106
|
|
|
); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
$collection = $this->reflection->getConstant('COLLECTION'); |
110
|
|
|
if (empty($collection)) { |
111
|
|
|
//Generate collection using short class name |
112
|
|
|
$collection = Inflector::camelize($this->reflection->getShortName()); |
113
|
|
|
$collection = Inflector::pluralize($collection); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return $collection; |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Get every embedded entity field (excluding declarations of aggregations). |
121
|
|
|
* |
122
|
|
|
* @return array |
123
|
|
|
*/ |
124
|
|
|
public function getFields(): array |
125
|
|
|
{ |
126
|
|
|
$fields = $this->reflection->getFields(); |
127
|
|
|
|
128
|
|
|
foreach ($fields as $field => $type) { |
129
|
|
|
if ($this->isAggregation($type)) { |
130
|
|
|
unset($fields[$field]); |
131
|
|
|
} |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
return $fields; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* {@inheritdoc} |
139
|
|
|
*/ |
140
|
|
|
public function getIndexes(): array |
141
|
|
|
{ |
142
|
|
|
if ($this->isEmbedded()) { |
143
|
|
|
throw new SchemaException( |
144
|
|
|
"Unable to get indexes for embedded model {$this->reflection}" |
145
|
|
|
); |
146
|
|
|
} |
147
|
|
|
|
148
|
|
|
$indexes = $this->reflection->getProperty('indexes', true); |
149
|
|
|
if (empty($indexes) || !is_array($indexes)) { |
150
|
|
|
return []; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$result = []; |
154
|
|
|
foreach ($indexes as $index) { |
155
|
|
|
$options = []; |
156
|
|
|
if (isset($index['@options'])) { |
157
|
|
|
$options = $index['@options']; |
158
|
|
|
unset($index['@options']); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
$result[] = new IndexDefinition($index, $options); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
return array_unique($result); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
/** |
168
|
|
|
* @return AggregationDefinition[] |
169
|
|
|
*/ |
170
|
|
|
public function getAggregations(): array |
171
|
|
|
{ |
172
|
|
|
$result = []; |
173
|
|
|
foreach ($this->reflection->getFields() as $field => $type) { |
174
|
|
|
if ($this->isAggregation($type)) { |
175
|
|
|
$aggregationType = isset($type[Document::ONE]) ? Document::ONE : Document::MANY; |
176
|
|
|
|
177
|
|
|
$result[$field] = new AggregationDefinition( |
178
|
|
|
$aggregationType, //Aggregation type |
179
|
|
|
$type[$aggregationType], //Class name |
180
|
|
|
array_pop($type) //Query template |
181
|
|
|
); |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
return $result; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Find all composition definitions, attention method require builder instance in order to |
190
|
|
|
* properly check that embedded class exists. |
191
|
|
|
* |
192
|
|
|
* @param SchemaBuilder $builder |
193
|
|
|
* |
194
|
|
|
* @return CompositionDefinition[] |
195
|
|
|
*/ |
196
|
|
|
public function getCompositions(SchemaBuilder $builder): array |
197
|
|
|
{ |
198
|
|
|
$result = []; |
199
|
|
|
foreach ($this->reflection->getFields() as $field => $type) { |
200
|
|
|
if (is_string($type) && $builder->hasSchema($type)) { |
201
|
|
|
$result[$field] = new CompositionDefinition(DocumentEntity::ONE, $type); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
if (is_array($type) && isset($type[0]) && $builder->hasSchema($type[0])) { |
205
|
|
|
$result[$field] = new CompositionDefinition(DocumentEntity::MANY, $type[0]); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
return $result; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* {@inheritdoc} |
214
|
|
|
*/ |
215
|
|
|
public function resolvePrimary(SchemaBuilder $builder): string |
216
|
|
|
{ |
217
|
|
|
//Let's define a way how to separate one model from another based on given fields |
218
|
|
|
$helper = new InheritanceHelper($this, $builder->getSchemas()); |
219
|
|
|
|
220
|
|
|
return $helper->findPrimary(); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* {@inheritdoc} |
225
|
|
|
*/ |
226
|
|
|
public function packSchema(SchemaBuilder $builder): array |
227
|
|
|
{ |
228
|
|
|
return [ |
229
|
|
|
//Instantion options and behaviour (if any) |
230
|
|
|
DocumentEntity::SH_INSTANTIATION => $this->instantiationOptions($builder), |
231
|
|
|
|
232
|
|
|
//Default entity state (builder is needed to resolve recursive defaults) |
233
|
|
|
DocumentEntity::SH_DEFAULTS => $this->packDefaults($builder), |
234
|
|
|
|
235
|
|
|
//Entity behaviour |
236
|
|
|
DocumentEntity::SH_HIDDEN => $this->reflection->getHidden(), |
237
|
|
|
DocumentEntity::SH_SECURED => $this->reflection->getSecured(), |
238
|
|
|
DocumentEntity::SH_FILLABLE => $this->reflection->getFillable(), |
239
|
|
|
|
240
|
|
|
//Mutators can be altered based on ODM\SchemasConfig |
241
|
|
|
DocumentEntity::SH_MUTATORS => $this->packMutators(), |
242
|
|
|
|
243
|
|
|
//Document behaviours (we can mix them with accessors due potential inheritance) |
244
|
|
|
DocumentEntity::SH_COMPOSITIONS => $this->packCompositions($builder), |
245
|
|
|
DocumentEntity::SH_AGGREGATIONS => $this->packAggregations($builder), |
246
|
|
|
]; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Define instantiator specific options (usually needed to resolve class inheritance). Might |
251
|
|
|
* return null if associated instantiator is unknown to DocumentSchema. |
252
|
|
|
* |
253
|
|
|
* @param SchemaBuilder $builder |
254
|
|
|
* |
255
|
|
|
* @return mixed |
256
|
|
|
*/ |
257
|
|
|
protected function instantiationOptions(SchemaBuilder $builder) |
258
|
|
|
{ |
259
|
|
|
if ($this->getInstantiator() != DocumentInstantiator::class) { |
260
|
|
|
//Unable to define options for non default inheritance based instantiator |
261
|
|
|
return null; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
//Let's define a way how to separate one model from another based on given fields |
265
|
|
|
$helper = new InheritanceHelper($this, $builder->getSchemas()); |
266
|
|
|
|
267
|
|
|
return $helper->makeDefinition(); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
/** |
271
|
|
|
* Entity default values. |
272
|
|
|
* |
273
|
|
|
* @param SchemaBuilder $builder |
274
|
|
|
* @param array $overwriteDefaults Set of default values to replace user defined values. |
275
|
|
|
* |
276
|
|
|
* @return array |
277
|
|
|
* |
278
|
|
|
* @throws SchemaException |
279
|
|
|
*/ |
280
|
|
|
protected function packDefaults(SchemaBuilder $builder, array $overwriteDefaults = []): array |
281
|
|
|
{ |
282
|
|
|
//Defined compositions |
283
|
|
|
$compositions = $this->getCompositions($builder); |
284
|
|
|
|
285
|
|
|
//User defined default values |
286
|
|
|
$userDefined = $overwriteDefaults + $this->reflection->getProperty('defaults'); |
287
|
|
|
|
288
|
|
|
//We need mutators to normalize default values |
289
|
|
|
$mutators = $this->packMutators(); |
290
|
|
|
|
291
|
|
|
$defaults = []; |
292
|
|
|
foreach ($this->getFields() as $field => $type) { |
293
|
|
|
$default = is_array($type) ? [] : null; |
294
|
|
|
|
295
|
|
|
if (array_key_exists($field, $userDefined)) { |
296
|
|
|
//No merge to keep fields order intact |
297
|
|
|
$default = $userDefined[$field]; |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
if (array_key_exists($field, $defaults)) { |
301
|
|
|
//Default value declared in model schema |
302
|
|
|
$default = $defaults[$field]; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
//Let's process default value using associated setter |
306
|
|
|
if (isset($mutators[DocumentEntity::MUTATOR_SETTER][$field])) { |
307
|
|
|
try { |
308
|
|
|
$setter = $mutators[DocumentEntity::MUTATOR_SETTER][$field]; |
309
|
|
|
$default = call_user_func($setter, $default); |
310
|
|
|
} catch (\Exception $exception) { |
311
|
|
|
//Unable to generate default value, use null or empty array as fallback |
312
|
|
|
} |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
if (isset($mutators[DocumentEntity::MUTATOR_ACCESSOR][$field])) { |
316
|
|
|
$accessor = $mutators[DocumentEntity::MUTATOR_ACCESSOR][$field]; |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @var AccessorInterface $instance |
320
|
|
|
*/ |
321
|
|
|
$instance = new $accessor($default, [/*no context given*/]); |
322
|
|
|
$default = $instance->packValue(); |
323
|
|
|
|
324
|
|
|
if (!is_scalar($default)) { |
325
|
|
|
//Some accessors might want to return objects (DateTime, StorageObject), default to null |
326
|
|
|
$default = null; |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
if (isset($compositions[$field])) { |
331
|
|
|
if (is_null($default) && !array_key_exists($field, $userDefined)) { |
332
|
|
|
//Let's force default value for composite fields |
333
|
|
|
$default = []; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$default = $this->resolveDefault($default, $compositions[$field], $builder); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
//Registering default values |
340
|
|
|
$defaults[$field] = $default; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
return $defaults; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Generate set of mutators associated with entity fields using user defined and automatic |
348
|
|
|
* mutators. |
349
|
|
|
* |
350
|
|
|
* @see MutatorsConfig |
351
|
|
|
* @return array |
352
|
|
|
*/ |
353
|
|
|
protected function packMutators(): array |
354
|
|
|
{ |
355
|
|
|
$mutators = $this->reflection->getMutators(); |
356
|
|
|
|
357
|
|
|
//Trying to resolve mutators based on field type |
358
|
|
|
foreach ($this->getFields() as $field => $type) { |
359
|
|
|
//Resolved mutators |
360
|
|
|
$resolved = []; |
361
|
|
|
|
362
|
|
|
if ( |
363
|
|
|
is_array($type) |
364
|
|
|
&& is_scalar($type[0]) |
365
|
|
|
&& $filter = $this->mutators->getMutators('array::' . $type[0]) |
366
|
|
|
) { |
367
|
|
|
//Mutator associated to array with specified type |
368
|
|
|
$resolved += $filter; |
369
|
|
|
} elseif (is_array($type) && $filter = $this->mutators->getMutators('array')) { |
370
|
|
|
//Default array mutator |
371
|
|
|
$resolved += $filter; |
372
|
|
|
} elseif (!is_array($type) && $filter = $this->mutators->getMutators($type)) { |
373
|
|
|
//Mutator associated with type directly |
374
|
|
|
$resolved += $filter; |
375
|
|
|
} |
376
|
|
|
|
377
|
|
|
//Merging mutators and default mutators |
378
|
|
|
foreach ($resolved as $mutator => $filter) { |
379
|
|
|
if (!array_key_exists($field, $mutators[$mutator])) { |
380
|
|
|
$mutators[$mutator][$field] = $filter; |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
|
385
|
|
|
//Some mutators may be described using aliases (for shortness) |
386
|
|
|
//$mutators = $this->normalizeMutators($mutators); |
|
|
|
|
387
|
|
|
|
388
|
|
|
return $mutators; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
/** |
392
|
|
|
* Pack compositions into simple array definition. |
393
|
|
|
* |
394
|
|
|
* @param SchemaBuilder $builder |
395
|
|
|
* |
396
|
|
|
* @return array |
397
|
|
|
* |
398
|
|
|
* @throws SchemaException |
399
|
|
|
*/ |
400
|
|
|
public function packCompositions(SchemaBuilder $builder): array |
401
|
|
|
{ |
402
|
|
|
$result = []; |
403
|
|
|
foreach ($this->getCompositions($builder) as $name => $composition) { |
404
|
|
|
$result[$name] = $composition->packSchema(); |
405
|
|
|
} |
406
|
|
|
|
407
|
|
|
return $result; |
408
|
|
|
} |
409
|
|
|
|
410
|
|
|
/** |
411
|
|
|
* Pack aggregations into simple array definition. |
412
|
|
|
* |
413
|
|
|
* @param SchemaBuilder $builder |
414
|
|
|
* |
415
|
|
|
* @return array |
416
|
|
|
* |
417
|
|
|
* @throws SchemaException |
418
|
|
|
*/ |
419
|
|
|
protected function packAggregations(SchemaBuilder $builder): array |
420
|
|
|
{ |
421
|
|
|
$result = []; |
422
|
|
|
foreach ($this->getAggregations() as $name => $aggregation) { |
423
|
|
|
if (!$builder->hasSchema($aggregation->getClass())) { |
424
|
|
|
throw new SchemaException( |
425
|
|
|
"Aggregation {$this->getClass()}.'{$name}' refers to undefined document '{$aggregation->getClass()}'" |
426
|
|
|
); |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
if ($builder->getSchema($aggregation->getClass())->isEmbedded()) { |
430
|
|
|
throw new SchemaException( |
431
|
|
|
"Aggregation {$this->getClass()}.'{$name}' refers to non storable document '{$aggregation->getClass()}'" |
432
|
|
|
); |
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
$result[$name] = $aggregation->packSchema(); |
436
|
|
|
} |
437
|
|
|
|
438
|
|
|
return $result; |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
/** |
442
|
|
|
* Check if field schema/type defines aggregation. |
443
|
|
|
* |
444
|
|
|
* @param mixed $type |
445
|
|
|
* |
446
|
|
|
* @return bool |
447
|
|
|
*/ |
448
|
|
|
protected function isAggregation($type): bool |
449
|
|
|
{ |
450
|
|
|
if (is_array($type)) { |
451
|
|
|
if (isset($type[Document::ONE]) || isset($type[Document::MANY])) { |
452
|
|
|
return true; |
453
|
|
|
} |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
return false; |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
|
|
* Ensure default value for composite field, |
461
|
|
|
* |
462
|
|
|
* @param mixed $default |
463
|
|
|
* @param CompositionDefinition $composition |
464
|
|
|
* @param SchemaBuilder $builder |
465
|
|
|
* |
466
|
|
|
* @return array |
467
|
|
|
* |
468
|
|
|
* @throws SchemaException |
469
|
|
|
*/ |
470
|
|
|
protected function resolveDefault( |
471
|
|
|
$default, |
472
|
|
|
CompositionDefinition $composition, |
473
|
|
|
SchemaBuilder $builder |
474
|
|
|
) { |
475
|
|
|
if (!is_array($default)) { |
476
|
|
|
if ($composition->getType() == DocumentEntity::MANY) { |
477
|
|
|
//Composition many must always defaults to array |
478
|
|
|
return []; |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
//Composite ONE must always defaults to null if no default value are specified |
482
|
|
|
return null; |
483
|
|
|
} |
484
|
|
|
|
485
|
|
|
//Nothing to do with value for composite many |
486
|
|
|
if ($composition->getType() == DocumentEntity::MANY) { |
487
|
|
|
return $default; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
$embedded = $builder->getSchema($composition->getClass()); |
491
|
|
|
if (!$embedded instanceof self) { |
492
|
|
|
//We can not normalize values handled by external schemas yet |
493
|
|
|
return $default; |
494
|
|
|
} |
495
|
|
|
|
496
|
|
|
if ($embedded->getClass() == $this->getClass()) { |
497
|
|
|
if (!empty($default)) { |
498
|
|
|
throw new SchemaException( |
499
|
|
|
"Possible recursion issue in '{$this->getClass()}', model refers to itself (has default value)" |
500
|
|
|
); |
501
|
|
|
} |
502
|
|
|
|
503
|
|
|
//No recursions! |
504
|
|
|
return null; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
return $embedded->packDefaults($builder, $default); |
508
|
|
|
} |
509
|
|
|
} |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.