Completed
Branch feature/split-orm (94afb7)
by Anton
03:11
created

DocumentSchema::packCompositions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
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);
1 ignored issue
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

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.

Loading history...
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
}