Completed
Branch feature/pre-split (60f5c0)
by Anton
03:19
created

DocumentSchema::resolveMutators()   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 View Code Duplication
    public function getFields(): array
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
126
    {
127
        $fields = $this->reflection->getFields();
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->getFields() 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->getFields() 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
     * {@inheritdoc}
225
     */
226
    public function resolvePrimary(SchemaBuilder $builder): string
227
    {
228
        //Let's define a way how to separate one model from another based on given fields
229
        $helper = new InheritanceHelper($this, $builder->getSchemas());
230
231
        return $helper->findPrimary();
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237
    public function packSchema(SchemaBuilder $builder): array
238
    {
239
        return [
240
            //Instantion options and behaviour (if any)
241
            DocumentEntity::SH_INSTANTIATION => $this->instantiationOptions($builder),
242
243
            //Default entity state (builder is needed to resolve recursive defaults)
244
            DocumentEntity::SH_DEFAULTS      => $this->packDefaults($builder),
245
246
            //Entity behaviour
247
            DocumentEntity::SH_HIDDEN        => $this->reflection->getHidden(),
248
            DocumentEntity::SH_SECURED       => $this->reflection->getSecured(),
249
            DocumentEntity::SH_FILLABLE      => $this->reflection->getFillable(),
250
251
            //Mutators can be altered based on ODM\SchemasConfig
252
            DocumentEntity::SH_MUTATORS      => $this->resolveMutators(),
253
254
            //Document behaviours (we can mix them with accessors due potential inheritance)
255
            DocumentEntity::SH_COMPOSITIONS  => $this->packCompositions($builder),
256
            DocumentEntity::SH_AGGREGATIONS  => $this->packAggregations($builder),
257
        ];
258
    }
259
260
    /**
261
     * Define instantiator specific options (usually needed to resolve class inheritance). Might
262
     * return null if associated instantiator is unknown to DocumentSchema.
263
     *
264
     * @param SchemaBuilder $builder
265
     *
266
     * @return mixed
267
     */
268
    protected function instantiationOptions(SchemaBuilder $builder)
269
    {
270
        if ($this->getInstantiator() != DocumentInstantiator::class) {
271
            //Unable to define options for non default inheritance based instantiator
272
            return null;
273
        }
274
275
        //Let's define a way how to separate one model from another based on given fields
276
        $helper = new InheritanceHelper($this, $builder->getSchemas());
277
278
        return $helper->makeDefinition();
279
    }
280
281
    /**
282
     * Entity default values.
283
     *
284
     * @param SchemaBuilder $builder
285
     * @param array         $overwriteDefaults Set of default values to replace user defined values.
286
     *
287
     * @return array
288
     *
289
     * @throws SchemaException
290
     */
291
    protected function packDefaults(SchemaBuilder $builder, array $overwriteDefaults = []): array
292
    {
293
        //Defined compositions
294
        $compositions = $this->getCompositions($builder);
295
296
        //User defined default values
297
        $userDefined = $overwriteDefaults + $this->getDefaults();
298
299
        //We need mutators to normalize default values
300
        $mutators = $this->resolveMutators();
301
302
        $defaults = [];
303
        foreach ($this->getFields() as $field => $type) {
304
            $default = is_array($type) ? [] : null;
305
306
            if (array_key_exists($field, $userDefined)) {
307
                //No merge to keep fields order intact
308
                $default = $userDefined[$field];
309
            }
310
311
            if (array_key_exists($field, $defaults)) {
312
                //Default value declared in model schema
313
                $default = $defaults[$field];
314
            }
315
316
            //Let's process default value using associated setter
317
            if (isset($mutators[DocumentEntity::MUTATOR_SETTER][$field])) {
318
                try {
319
                    $setter = $mutators[DocumentEntity::MUTATOR_SETTER][$field];
320
                    $default = call_user_func($setter, $default);
321
                } catch (\Exception $exception) {
322
                    //Unable to generate default value, use null or empty array as fallback
323
                }
324
            }
325
326
            if (isset($mutators[DocumentEntity::MUTATOR_ACCESSOR][$field])) {
327
                $default = $this->accessorDefault(
328
                    $default,
329
                    $mutators[DocumentEntity::MUTATOR_ACCESSOR][$field]
330
                );
331
            }
332
333
            if (isset($compositions[$field])) {
334
                if (is_null($default) && !array_key_exists($field, $userDefined)) {
335
                    //Let's force default value for composite fields
336
                    $default = [];
337
                }
338
339
                $default = $this->compositionDefault($default, $compositions[$field], $builder);
340
            }
341
342
            //Registering default values
343
            $defaults[$field] = $default;
344
        }
345
346
        return $defaults;
347
    }
348
349
    /**
350
     * Generate set of mutators associated with entity fields using user defined and automatic
351
     * mutators.
352
     *
353
     * @see MutatorsConfig
354
     * @return array
355
     */
356
    protected function resolveMutators(): array
357
    {
358
        $mutators = $this->reflection->getMutators();
359
360
        //Trying to resolve mutators based on field type
361
        foreach ($this->getFields() as $field => $type) {
362
            //Resolved mutators
363
            $resolved = [];
364
365
            if (
366
                is_array($type)
367
                && is_scalar($type[0])
368
                && $filter = $this->mutatorsConfig->getMutators('array::' . $type[0])
369
            ) {
370
                //Mutator associated to array with specified type
371
                $resolved += $filter;
372
            } elseif (is_array($type) && $filter = $this->mutatorsConfig->getMutators('array')) {
373
                //Default array mutator
374
                $resolved += $filter;
375
            } elseif (!is_array($type) && $filter = $this->mutatorsConfig->getMutators($type)) {
376
                //Mutator associated with type directly
377
                $resolved += $filter;
378
            }
379
380
            //Merging mutators and default mutators
381
            foreach ($resolved as $mutator => $filter) {
382
                if (!array_key_exists($field, $mutators[$mutator])) {
383
                    $mutators[$mutator][$field] = $filter;
384
                }
385
            }
386
        }
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
     * 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
}