Completed
Branch feature/pre-split (5f3640)
by Anton
03:19
created

DocumentSchema::getMutators()   C

Complexity

Conditions 11
Paths 13

Size

Total Lines 34
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 17
nc 13
nop 0
dl 0
loc 34
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Spiral Framework, Core 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\AccessorException;
17
use Spiral\ODM\Exceptions\SchemaException;
18
use Spiral\ODM\Schemas\Definitions\AggregationDefinition;
19
use Spiral\ODM\Schemas\Definitions\CompositionDefinition;
20
use Spiral\ODM\Schemas\Definitions\IndexDefinition;
21
22
class DocumentSchema implements SchemaInterface
23
{
24
    /**
25
     * @var ReflectionEntity
26
     */
27
    private $reflection;
28
29
    /**
30
     * @invisible
31
     *
32
     * @var MutatorsConfig
33
     */
34
    private $mutatorsConfig;
35
36
    /**
37
     * @param ReflectionEntity $reflection
38
     * @param MutatorsConfig   $mutators
39
     */
40
    public function __construct(ReflectionEntity $reflection, MutatorsConfig $mutators)
41
    {
42
        $this->reflection = $reflection;
43
        $this->mutatorsConfig = $mutators;
44
    }
45
46
    /**
47
     * @return string
48
     */
49
    public function getClass(): string
50
    {
51
        return $this->reflection->getName();
52
    }
53
54
    /**
55
     * @return ReflectionEntity
56
     */
57
    public function getReflection(): ReflectionEntity
58
    {
59
        return $this->reflection;
60
    }
61
62
    /**
63
     * @return string
64
     */
65
    public function getInstantiator(): string
66
    {
67
        return $this->reflection->getProperty('instantiator') ?? DocumentInstantiator::class;
68
    }
69
70
    /**
71
     * {@inheritdoc}
72
     */
73
    public function isEmbedded(): bool
74
    {
75
        return !$this->reflection->isSubclassOf(Document::class)
76
            && $this->reflection->isSubclassOf(DocumentEntity::class);
77
    }
78
79
    /**
80
     * {@inheritdoc}
81
     */
82
    public function getDatabase()
83
    {
84
        if ($this->isEmbedded()) {
85
            throw new SchemaException(
86
                "Unable to get database name for embedded model {$this->reflection}"
87
            );
88
        }
89
90
        $database = $this->reflection->getProperty('database');
91
        if (empty($database)) {
92
            //Empty database to be used
93
            return null;
94
        }
95
96
        return $database;
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102
    public function getCollection(): string
103
    {
104
        if ($this->isEmbedded()) {
105
            throw new SchemaException(
106
                "Unable to get collection name for embedded model {$this->reflection}"
107
            );
108
        }
109
110
        $collection = $this->reflection->getProperty('collection');
111
        if (empty($collection)) {
112
            //Generate collection using short class name
113
            $collection = Inflector::camelize($this->reflection->getShortName());
114
            $collection = Inflector::pluralize($collection);
115
        }
116
117
        return $collection;
118
    }
119
120
    /**
121
     * Get every embedded entity field (excluding declarations of aggregations).
122
     *
123
     * @return array
124
     */
125
    public function getFields(): array
126
    {
127
        $fields = $this->reflection->getSchema();
128
129
        foreach ($fields as $field => $type) {
130
            if ($this->isAggregation($type)) {
131
                unset($fields[$field]);
132
            }
133
        }
134
135
        return $fields;
136
    }
137
138
    /**
139
     * Default defined values.
140
     *
141
     * @return array
142
     */
143
    public function getDefaults(): array
144
    {
145
        return $this->reflection->getProperty('defaults') ?? [];
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151
    public function getIndexes(): array
152
    {
153
        if ($this->isEmbedded()) {
154
            throw new SchemaException(
155
                "Unable to get indexes for embedded model {$this->reflection}"
156
            );
157
        }
158
159
        $indexes = $this->reflection->getProperty('indexes', true);
160
        if (empty($indexes) || !is_array($indexes)) {
161
            return [];
162
        }
163
164
        $result = [];
165
        foreach ($indexes as $index) {
166
            $options = [];
167
            if (isset($index['@options'])) {
168
                $options = $index['@options'];
169
                unset($index['@options']);
170
            }
171
172
            $result[] = new IndexDefinition($index, $options);
173
        }
174
175
        return array_unique($result);
176
    }
177
178
    /**
179
     * @return AggregationDefinition[]
180
     */
181
    public function getAggregations(): array
182
    {
183
        $result = [];
184
        foreach ($this->reflection->getSchema() as $field => $type) {
185
            if ($this->isAggregation($type)) {
186
                $aggregationType = isset($type[Document::ONE]) ? Document::ONE : Document::MANY;
187
188
                $result[$field] = new AggregationDefinition(
189
                    $aggregationType,        //Aggregation type
190
                    $type[$aggregationType], //Class name
191
                    array_pop($type)         //Query template
192
                );
193
            }
194
        }
195
196
        return $result;
197
    }
198
199
    /**
200
     * Find all composition definitions, attention method require builder instance in order to
201
     * properly check that embedded class exists.
202
     *
203
     * @param SchemaBuilder $builder
204
     *
205
     * @return CompositionDefinition[]
206
     */
207
    public function getCompositions(SchemaBuilder $builder): array
208
    {
209
        $result = [];
210
        foreach ($this->reflection->getSchema() as $field => $type) {
211
            if (is_string($type) && $builder->hasSchema($type)) {
212
                $result[$field] = new CompositionDefinition(DocumentEntity::ONE, $type);
213
            }
214
215
            if (is_array($type) && isset($type[0]) && $builder->hasSchema($type[0])) {
216
                $result[$field] = new CompositionDefinition(DocumentEntity::MANY, $type[0]);
217
            }
218
        }
219
220
        return $result;
221
    }
222
223
    /**
224
     * Generate set of mutators associated with entity fields using user defined and automatic
225
     * mutators.
226
     *
227
     * @see MutatorsConfig
228
     * @return array
229
     */
230
    public function getMutators(): array
231
    {
232
        $mutators = $this->reflection->getMutators();
233
234
        //Trying to resolve mutators based on field type
235
        foreach ($this->getFields() as $field => $type) {
236
            //Resolved mutators
237
            $resolved = [];
238
239
            if (
240
                is_array($type)
241
                && is_scalar($type[0])
242
                && $filter = $this->mutatorsConfig->getMutators('array::' . $type[0])
243
            ) {
244
                //Mutator associated to array with specified type
245
                $resolved += $filter;
246
            } elseif (is_array($type) && $filter = $this->mutatorsConfig->getMutators('array')) {
247
                //Default array mutator
248
                $resolved += $filter;
249
            } elseif (!is_array($type) && $filter = $this->mutatorsConfig->getMutators($type)) {
250
                //Mutator associated with type directly
251
                $resolved += $filter;
252
            }
253
254
            //Merging mutators and default mutators
255
            foreach ($resolved as $mutator => $filter) {
256
                if (!array_key_exists($field, $mutators[$mutator])) {
257
                    $mutators[$mutator][$field] = $filter;
258
                }
259
            }
260
        }
261
262
        return $mutators;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function resolvePrimary(SchemaBuilder $builder): string
269
    {
270
        //Let's define a way how to separate one model from another based on given fields
271
        $helper = new InheritanceHelper($this, $builder->getSchemas());
272
273
        return $helper->findPrimary();
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function packSchema(SchemaBuilder $builder): array
280
    {
281
        return [
282
            //Instantion options and behaviour (if any)
283
            DocumentEntity::SH_INSTANTIATION => $this->instantiationOptions($builder),
284
285
            //Default entity state (builder is needed to resolve recursive defaults)
286
            DocumentEntity::SH_DEFAULTS      => $this->packDefaults($builder),
287
288
            //Entity behaviour
289
            DocumentEntity::SH_HIDDEN        => $this->reflection->getHidden(),
290
            DocumentEntity::SH_SECURED       => $this->reflection->getSecured(),
291
            DocumentEntity::SH_FILLABLE      => $this->reflection->getFillable(),
292
293
            //Mutators can be altered based on ODM\SchemasConfig
294
            DocumentEntity::SH_MUTATORS      => $this->getMutators(),
295
296
            //Document behaviours (we can mix them with accessors due potential inheritance)
297
            DocumentEntity::SH_COMPOSITIONS  => $this->packCompositions($builder),
298
            DocumentEntity::SH_AGGREGATIONS  => $this->packAggregations($builder),
299
        ];
300
    }
301
302
    /**
303
     * Define instantiator specific options (usually needed to resolve class inheritance). Might
304
     * return null if associated instantiator is unknown to DocumentSchema.
305
     *
306
     * @param SchemaBuilder $builder
307
     *
308
     * @return mixed
309
     */
310
    protected function instantiationOptions(SchemaBuilder $builder)
311
    {
312
        if ($this->getInstantiator() != DocumentInstantiator::class) {
313
            //Unable to define options for non default inheritance based instantiator
314
            return null;
315
        }
316
317
        //Let's define a way how to separate one model from another based on given fields
318
        $helper = new InheritanceHelper($this, $builder->getSchemas());
319
320
        return $helper->makeDefinition();
321
    }
322
323
    /**
324
     * Entity default values.
325
     *
326
     * @param SchemaBuilder $builder
327
     * @param array         $overwriteDefaults Set of default values to replace user defined values.
328
     *
329
     * @return array
330
     *
331
     * @throws SchemaException
332
     */
333
    protected function packDefaults(SchemaBuilder $builder, array $overwriteDefaults = []): array
334
    {
335
        //Defined compositions
336
        $compositions = $this->getCompositions($builder);
337
338
        //User defined default values
339
        $userDefined = $overwriteDefaults + $this->getDefaults();
340
341
        //We need mutators to normalize default values
342
        $mutators = $this->getMutators();
343
344
        $defaults = [];
345
        foreach ($this->getFields() as $field => $type) {
346
            $default = is_array($type) ? [] : null;
347
348
            if (array_key_exists($field, $userDefined)) {
349
                //No merge to keep fields order intact
350
                $default = $userDefined[$field];
351
            }
352
353
            if (array_key_exists($field, $defaults)) {
354
                //Default value declared in model schema
355
                $default = $defaults[$field];
356
            }
357
358
            //Let's process default value using associated setter
359
            if (isset($mutators[DocumentEntity::MUTATOR_SETTER][$field])) {
360
                try {
361
                    $setter = $mutators[DocumentEntity::MUTATOR_SETTER][$field];
362
                    $default = call_user_func($setter, $default);
363
                } catch (\Exception $exception) {
364
                    //Unable to generate default value, use null or empty array as fallback
365
                }
366
            }
367
368
            if (isset($mutators[DocumentEntity::MUTATOR_ACCESSOR][$field])) {
369
                $default = $this->accessorDefault(
370
                    $default,
371
                    $mutators[DocumentEntity::MUTATOR_ACCESSOR][$field]
372
                );
373
            }
374
375
            if (isset($compositions[$field])) {
376
                if (is_null($default) && !array_key_exists($field, $userDefined)) {
377
                    //Let's force default value for composite fields
378
                    $default = [];
379
                }
380
381
                $default = $this->compositionDefault($default, $compositions[$field], $builder);
382
            }
383
384
            //Registering default values
385
            $defaults[$field] = $default;
386
        }
387
388
        return $defaults;
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
     * Pass value thought accessor to ensure it's default.
461
     *
462
     * @param mixed  $default
463
     * @param string $accessor
464
     *
465
     * @return mixed
466
     *
467
     * @throws AccessorException
468
     */
469
    protected function accessorDefault($default, string $accessor)
470
    {
471
        /**
472
         * @var AccessorInterface $instance
473
         */
474
        $instance = new $accessor($default, [/*no context given*/]);
475
        $default = $instance->packValue();
476
477
        if (is_object($default)) {
478
            //Some accessors might want to return objects (DateTime, StorageObject), default to null
479
            $default = null;
480
        }
481
482
        return $default;
483
    }
484
485
    /**
486
     * Ensure default value for composite field,
487
     *
488
     * @param mixed                 $default
489
     * @param CompositionDefinition $composition
490
     * @param SchemaBuilder         $builder
491
     *
492
     * @return array
493
     *
494
     * @throws SchemaException
495
     */
496
    protected function compositionDefault(
497
        $default,
498
        CompositionDefinition $composition,
499
        SchemaBuilder $builder
500
    ) {
501
        if (!is_array($default)) {
502
            if ($composition->getType() == DocumentEntity::MANY) {
503
                //Composition many must always defaults to array
504
                return [];
505
            }
506
507
            //Composite ONE must always defaults to null if no default value are specified
508
            return null;
509
        }
510
511
        //Nothing to do with value for composite many
512
        if ($composition->getType() == DocumentEntity::MANY) {
513
            return $default;
514
        }
515
516
        $embedded = $builder->getSchema($composition->getClass());
517
        if (!$embedded instanceof self) {
518
            //We can not normalize values handled by external schemas yet
519
            return $default;
520
        }
521
522
        if ($embedded->getClass() == $this->getClass()) {
523
            if (!empty($default)) {
524
                throw new SchemaException(
525
                    "Possible recursion issue in '{$this->getClass()}', model refers to itself (has default value)"
526
                );
527
            }
528
529
            //No recursions!
530
            return null;
531
        }
532
533
        return $embedded->packDefaults($builder, $default);
534
    }
535
}