Completed
Push — master ( f90cdd...fed472 )
by Anton
02:13
created

DocumentSchema   C

Complexity

Total Complexity 79

Size/Duplication

Total Lines 549
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
wmc 79
lcom 1
cbo 11
dl 0
loc 549
rs 5.1632
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getClass() 0 4 1
A getReflection() 0 4 1
A getInstantiator() 0 4 1
A isEmbedded() 0 5 2
A getDatabase() 0 16 3
B getCollection() 0 23 4
A getFields() 0 12 3
A getDefaults() 0 4 1
B getIndexes() 0 26 6
A getAggregations() 0 17 4
B getCompositions() 0 15 7
C getMutators() 0 34 11
A resolvePrimary() 0 7 1
A packSchema() 0 22 1
A instantiationOptions() 0 12 2
B packDefaults() 0 33 4
A packCompositions() 0 9 2
A packAggregations() 0 21 4
A isAggregation() 0 10 4
C mutateValue() 0 40 7
A accessorDefault() 0 15 2
C compositionDefault() 0 39 7

How to fix   Complexity   

Complex Class

Complex classes like DocumentSchema often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentSchema, and based on these observations, apply Extract Interface, too.

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