Completed
Branch feature/pre-split (656bce)
by Anton
04:24
created

RecordSchema::mutateValue()   B

Complexity

Conditions 4
Paths 7

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 7
nop 3
dl 0
loc 25
rs 8.5806
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas;
9
10
use Doctrine\Common\Inflector\Inflector;
11
use Spiral\Database\Schemas\Prototypes\AbstractTable;
12
use Spiral\Models\AccessorInterface;
13
use Spiral\Models\Exceptions\AccessException;
14
use Spiral\Models\Reflections\ReflectionEntity;
15
use Spiral\ORM\Configs\MutatorsConfig;
16
use Spiral\ORM\Entities\RecordInstantiator;
17
use Spiral\ORM\Exceptions\DefinitionException;
18
use Spiral\ORM\Helpers\ColumnRenderer;
19
use Spiral\ORM\Record;
20
use Spiral\ORM\RecordAccessorInterface;
21
use Spiral\ORM\RecordEntity;
22
use Spiral\ORM\Schemas\Definitions\IndexDefinition;
23
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
24
25
class RecordSchema implements SchemaInterface
26
{
27
    /**
28
     * @var ReflectionEntity
29
     */
30
    private $reflection;
31
32
    /**
33
     * @invisible
34
     * @var MutatorsConfig
35
     */
36
    private $mutatorsConfig;
37
38
    /**
39
     * @invisible
40
     * @var ColumnRenderer
41
     */
42
    private $renderer;
43
44
    /**
45
     * @param ReflectionEntity    $reflection
46
     * @param MutatorsConfig      $mutators
47
     * @param ColumnRenderer|null $rendered
48
     */
49
    public function __construct(
50
        ReflectionEntity $reflection,
51
        MutatorsConfig $mutators,
52
        ColumnRenderer $rendered = null
53
    ) {
54
        $this->reflection = $reflection;
55
        $this->mutatorsConfig = $mutators;
56
        $this->renderer = $rendered ?? new ColumnRenderer();
57
    }
58
59
    /**
60
     * {@inheritdoc}
61
     */
62
    public function getClass(): string
63
    {
64
        return $this->reflection->getName();
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function getRole(): string
71
    {
72
        $role = $this->reflection->getProperty('model_role');
73
74
        //When role not defined we are going to use short class name
75
        return $role ?? lcfirst($this->reflection->getShortName());
76
    }
77
78
    /**
79
     * @return ReflectionEntity
80
     */
81
    public function getReflection(): ReflectionEntity
82
    {
83
        return $this->reflection;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function getInstantiator(): string
90
    {
91
        return $this->reflection->getProperty('instantiator') ?? RecordInstantiator::class;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function getDatabase()
98
    {
99
        $database = $this->reflection->getProperty('database');
100
        if (empty($database)) {
101
            //Empty database to be used
102
            return null;
103
        }
104
105
        return $database;
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function getTable(): string
112
    {
113
        $table = $this->reflection->getProperty('table');
114
        if (empty($table)) {
115
            //Generate collection using short class name
116
            $table = Inflector::camelize($this->reflection->getShortName());
117
            $table = Inflector::pluralize($table);
118
        }
119
120
        return $table;
121
    }
122
123
    /**
124
     * Fields and their types declared in Record model.
125
     *
126
     * @return array
127
     */
128
    public function getFields(): array
129
    {
130
        $fields = $this->reflection->getSchema();
131
132
        foreach ($fields as $field => $type) {
133
            if ($this->isRelation($type)) {
134
                unset($fields[$field]);
135
            }
136
        }
137
138
        return $fields;
139
    }
140
141
    /**
142
     * Returns set of declared indexes.
143
     *
144
     * Example:
145
     * const INDEXES = [
146
     *      [self::UNIQUE, 'email'],
147
     *      [self::INDEX, 'status', 'balance'],
148
     *      [self::INDEX, 'public_id']
149
     * ];
150
     *
151
     * @do generator
152
     *
153
     * @return \Generator|IndexDefinition[]
154
     *
155
     * @throws DefinitionException
156
     */
157
    public function getIndexes(): \Generator
158
    {
159
        $definitions = $this->reflection->getProperty('indexes') ?? [];
160
161
        foreach ($definitions as $definition) {
162
            yield $this->castIndex($definition);
163
        }
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function declareTable(AbstractTable $table): AbstractTable
170
    {
171
        return $this->renderer->renderColumns(
172
            $this->getFields(),
173
            $this->getDefaults(),
174
            $table
175
        );
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function getRelations(): \Generator
182
    {
183
        $schema = $this->reflection->getSchema();
184
185
        foreach ($schema as $name => $definition) {
186
            if (!$this->isRelation($definition)) {
187
                continue;
188
            }
189
190
            /**
191
             * We expect relations to be defined in a following form:
192
             *
193
             * [type => target, option => value, option => value]
194
             */
195
            $type = key($definition);
196
            $target = $definition[$type];
197
            unset($definition[$type]);
198
199
            //Defining relation
200
            yield new RelationDefinition(
201
                $name,
202
                $type,
203
                $target,
204
                $definition,
205
                $definition[Record::INVERSE] ?? null
206
            );
207
        }
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function packSchema(SchemaBuilder $builder, AbstractTable $table): array
214
    {
215
        return [
216
            RecordEntity::SH_PRIMARY_KEY => current($table->getPrimaryKeys()),
217
218
            //Default entity values
219
            RecordEntity::SH_DEFAULTS    => $this->packDefaults($table),
220
221
            //Entity behaviour
222
            RecordEntity::SH_HIDDEN      => $this->reflection->getHidden(),
223
            RecordEntity::SH_SECURED     => $this->reflection->getSecured(),
224
            RecordEntity::SH_FILLABLE    => $this->reflection->getFillable(),
225
226
            //Mutators can be altered based on ORM\SchemasConfig
227
            RecordEntity::SH_MUTATORS    => $this->buildMutators($table),
228
        ];
229
    }
230
231
    /**
232
     * Generate set of default values to be used by record.
233
     *
234
     * @param AbstractTable $table
235
     *
236
     * @return array
237
     */
238
    protected function packDefaults(AbstractTable $table): array
239
    {
240
        //We need mutators to normalize default values
241
        $mutators = $this->buildMutators($table);
242
243
        $defaults = [];
244
        foreach ($table->getColumns() as $column) {
245
            $field = $column->getName();
246
247
            $default = $column->getDefaultValue();
248
249
            //For non null values let's apply mutators to typecast it
250
            if (!is_null($default) && !is_object($default) && !$column->isNullable()) {
251
                $default = $this->mutateValue($mutators, $field, $default);
252
            }
253
254
            $defaults[$field] = $default;
255
        }
256
257
        return $defaults;
258
    }
259
260
    /**
261
     * Generate set of mutators associated with entity fields using user defined and automatic
262
     * mutators.
263
     *
264
     * @see MutatorsConfig
265
     *
266
     * @param AbstractTable $table
267
     *
268
     * @return array
269
     */
270
    protected function buildMutators(AbstractTable $table): array
271
    {
272
        $mutators = $this->reflection->getMutators();
273
274
        //Trying to resolve mutators based on field type
275
        foreach ($table->getColumns() as $column) {
276
            //Resolved mutators
277
            $resolved = [];
278
279
            if (!empty($filter = $this->mutatorsConfig->getMutators($column->abstractType()))) {
280
                //Mutator associated with type directly
281
                $resolved += $filter;
282
            } elseif (!empty($filter = $this->mutatorsConfig->getMutators('php:' . $column->phpType()))) {
283
                //Mutator associated with php type
284
                $resolved += $filter;
285
            }
286
287
            //Merging mutators and default mutators
288
            foreach ($resolved as $mutator => $filter) {
289
                if (!array_key_exists($column->getName(), $mutators[$mutator])) {
290
                    $mutators[$mutator][$column->getName()] = $filter;
291
                }
292
            }
293
        }
294
295
        foreach ($this->getFields() as $field => $type) {
296
            if (
297
                class_exists($type)
298
                && is_a($type, RecordAccessorInterface::class, true)
299
            ) {
300
                //Direct column accessor definition
301
                $mutators['accessor'][$field] = $type;
302
            }
303
        }
304
305
        return $mutators;
306
    }
307
308
    /**
309
     * Check if field schema/type defines relation.
310
     *
311
     * @param mixed $type
312
     *
313
     * @return bool
314
     */
315
    protected function isRelation($type): bool
316
    {
317
        if (is_array($type)) {
318
            return true;
319
        }
320
321
        return false;
322
    }
323
324
    /**
325
     * @param array $definition
326
     *
327
     * @return IndexDefinition
328
     *
329
     * @throws DefinitionException
330
     */
331
    protected function castIndex(array $definition)
332
    {
333
        $unique = null;
334
        $columns = [];
335
336
        foreach ($definition as $chunk) {
337
            if ($chunk == RecordEntity::INDEX || $chunk == RecordEntity::UNIQUE) {
338
                $unique = $chunk === RecordEntity::UNIQUE;
339
                continue;
340
            }
341
342
            $columns[] = $chunk;
343
        }
344
345
        if (is_null($unique)) {
346
            throw new DefinitionException(
347
                "Record '{$this}' has index definition with unspecified index type"
348
            );
349
        }
350
351
        if (empty($columns)) {
352
            throw new DefinitionException(
353
                "Record '{$this}' has index definition without any column associated to"
354
            );
355
        }
356
357
        return new IndexDefinition($columns, $unique);
358
    }
359
360
    /**
361
     * Default defined values.
362
     *
363
     * @return array
364
     */
365
    protected function getDefaults(): array
366
    {
367
        //Process defaults
368
        return $this->reflection->getProperty('defaults') ?? [];
369
    }
370
371
    /**
372
     * Process value thought associated mutator if any.
373
     *
374
     * @param array  $mutators
375
     * @param string $field
376
     * @param mixed  $default
377
     *
378
     * @return mixed
379
     */
380
    protected function mutateValue(array $mutators, string $field, $default)
381
    {
382
        //Let's process default value using associated setter
383
        if (isset($mutators[RecordEntity::MUTATOR_SETTER][$field])) {
384
            try {
385
                $setter = $mutators[RecordEntity::MUTATOR_SETTER][$field];
386
                $default = call_user_func($setter, $default);
387
388
                return $default;
389
            } catch (\Exception $exception) {
390
                //Unable to generate default value, use null or empty array as fallback
391
            }
392
        }
393
394
        if (isset($mutators[RecordEntity::MUTATOR_ACCESSOR][$field])) {
395
            $default = $this->accessorDefault(
396
                $default,
397
                $mutators[RecordEntity::MUTATOR_ACCESSOR][$field]
398
            );
399
400
            return $default;
401
        }
402
403
        return $default;
404
    }
405
406
    /**
407
     * Pass value thought accessor to ensure it's default.
408
     *
409
     * @param mixed  $default
410
     * @param string $accessor
411
     *
412
     * @return mixed
413
     *
414
     * @throws AccessException
415
     */
416
    protected function accessorDefault($default, string $accessor)
417
    {
418
        /**
419
         * @var AccessorInterface $instance
420
         */
421
        $instance = new $accessor($default, [/*no context given*/]);
422
        $default = $instance->packValue();
423
424
        if (is_object($default)) {
425
            //Some accessors might want to return objects (DateTime, StorageObject), default to null
426
            $default = null;
427
        }
428
429
        return $default;
430
    }
431
}