RecordSchema::getRole()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
/**
3
 * Spiral, Core Components
4
 *
5
 * @author Wolfy-J
6
 */
7
8
namespace Spiral\ORM\Schemas;
9
10
use Doctrine\Common\Inflector\Inflector;
11
use Spiral\Database\Configs\DatabasesConfig;
12
use Spiral\Database\Schemas\Prototypes\AbstractTable;
13
use Spiral\Models\AccessorInterface;
14
use Spiral\Models\Exceptions\AccessException;
15
use Spiral\Models\Reflections\ReflectionEntity;
16
use Spiral\ORM\Configs\MutatorsConfig;
17
use Spiral\ORM\Entities\RecordInstantiator;
18
use Spiral\ORM\Exceptions\DefinitionException;
19
use Spiral\ORM\Helpers\ColumnRenderer;
20
use Spiral\ORM\Record;
21
use Spiral\ORM\RecordAccessorInterface;
22
use Spiral\ORM\RecordEntity;
23
use Spiral\ORM\Schemas\Definitions\IndexDefinition;
24
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
25
26
class RecordSchema implements SchemaInterface
27
{
28
    /**
29
     * @var ReflectionEntity
30
     */
31
    private $reflection;
32
33
    /**
34
     * @invisible
35
     * @var MutatorsConfig
36
     */
37
    private $mutatorsConfig;
38
39
    /**
40
     * @invisible
41
     * @var ColumnRenderer
42
     */
43
    private $renderer;
44
45
    /**
46
     * @param ReflectionEntity    $reflection
47
     * @param MutatorsConfig      $mutators
48
     * @param ColumnRenderer|null $rendered
49
     */
50
    public function __construct(
51
        ReflectionEntity $reflection,
52
        MutatorsConfig $mutators,
53
        ColumnRenderer $rendered = null
54
    ) {
55
        $this->reflection = $reflection;
56
        $this->mutatorsConfig = $mutators;
57
        $this->renderer = $rendered ?? new ColumnRenderer();
58
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63
    public function getClass(): string
64
    {
65
        return $this->reflection->getName();
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function getRole(): string
72
    {
73
        $role = $this->reflection->getProperty('model_role');
74
75
        //When role not defined we are going to use short class name
76
        return $role ?? lcfirst(Inflector::tableize($this->reflection->getShortName()));
77
    }
78
79
    /**
80
     * @return ReflectionEntity
81
     */
82
    public function getReflection(): ReflectionEntity
83
    {
84
        return $this->reflection;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function getInstantiator(): string
91
    {
92
        return $this->reflection->getProperty('instantiator') ?? RecordInstantiator::class;
93
    }
94
95
    /**
96
     * {@inheritdoc}
97
     */
98
    public function getDatabase()
99
    {
100
        $database = $this->reflection->getProperty('database');
101
        if (empty($database)) {
102
            //Empty database to be used
103
            return null;
104
        }
105
106
        return $database;
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function getTable(): string
113
    {
114
        $table = $this->reflection->getProperty('table');
115
        if (empty($table)) {
116
            //Generate table using short class name
117
            $table = Inflector::tableize($this->reflection->getShortName());
118
            $table = Inflector::pluralize($table);
119
        }
120
121
        return $table;
122
    }
123
124
    /**
125
     * Fields and their types declared in Record model.
126
     *
127
     * @return array
128
     */
129
    public function getFields(): array
130
    {
131
        $fields = $this->reflection->getSchema();
132
133
        foreach ($fields as $field => $type) {
134
            if ($this->isRelation($type)) {
135
                unset($fields[$field]);
136
            }
137
        }
138
139
        return $fields;
140
    }
141
142
    /**
143
     * Returns set of declared indexes.
144
     *
145
     * Example:
146
     * const INDEXES = [
147
     *      [self::UNIQUE, 'email'],
148
     *      [self::INDEX, 'status', 'balance'],
149
     *      [self::INDEX, 'public_id']
150
     * ];
151
     *
152
     * @do generator
153
     *
154
     * @return \Generator|IndexDefinition[]
155
     *
156
     * @throws DefinitionException
157
     */
158
    public function getIndexes(): \Generator
159
    {
160
        $definitions = $this->reflection->getProperty('indexes') ?? [];
161
162
        foreach ($definitions as $definition) {
163
            yield $this->castIndex($definition);
164
        }
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170
    public function declareTable(AbstractTable $table): AbstractTable
171
    {
172
        return $this->renderer->renderColumns(
173
            $this->getFields(),
174
            $this->getDefaults(),
175
            $table
176
        );
177
    }
178
179
    /**
180
     * {@inheritdoc}
181
     */
182
    public function getRelations(): \Generator
183
    {
184
        $schema = $this->reflection->getSchema();
185
186
        foreach ($schema as $name => $definition) {
187
            if (!$this->isRelation($definition)) {
188
                continue;
189
            }
190
191
            /**
192
             * We expect relations to be defined in a following form:
193
             *
194
             * [type => target, option => value, option => value]
195
             */
196
            $type = key($definition);
197
            $target = $definition[$type];
198
            unset($definition[$type]);
199
200
            //Defining relation
201
            yield new RelationDefinition(
202
                $name,
203
                $type,
204
                $target,
205
                $definition,
206
                $definition[Record::INVERSE] ?? null
207
            );
208
        }
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214
    public function packSchema(SchemaBuilder $builder, AbstractTable $table): array
215
    {
216
        return [
217
            RecordEntity::SH_PRIMARY_KEY => current($table->getPrimaryKeys()),
218
219
            //Default entity values
220
            RecordEntity::SH_DEFAULTS    => $this->packDefaults($table),
221
222
            //Entity behaviour
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
        $name = null;
334
        $unique = null;
335
        $columns = [];
336
337
338
        foreach ($definition as $key => $value) {
339
340
            if ($key == RecordEntity::INDEX || $key == RecordEntity::UNIQUE) {
341
                $unique = ($key === RecordEntity::UNIQUE);
342
343
                if (!is_string($value) || empty($value)){
344
                    throw new DefinitionException(
345
                        "Record '{$this}' has index definition with invalid index name"
346
                    );
347
                }
348
349
                $name = $value;
350
                continue;
351
            }
352
353
            if ($value == RecordEntity::INDEX || $value == RecordEntity::UNIQUE) {
354
                $unique = ($value === RecordEntity::UNIQUE);
355
                continue;
356
            }
357
358
            $columns[] = $value;
359
        }
360
361
        if (is_null($unique)) {
362
            throw new DefinitionException(
363
                "Record '{$this}' has index definition with unspecified index type"
364
            );
365
        }
366
367
        if (empty($columns)) {
368
            throw new DefinitionException(
369
                "Record '{$this}' has index definition without any column associated to"
370
            );
371
        }
372
373
        return new IndexDefinition($columns, $unique, $name);
374
    }
375
376
    /**
377
     * Default defined values.
378
     *
379
     * @return array
380
     */
381
    protected function getDefaults(): array
382
    {
383
        //Process defaults
384
        return $this->reflection->getProperty('defaults') ?? [];
385
    }
386
387
    /**
388
     * Process value thought associated mutator if any.
389
     *
390
     * @param array  $mutators
391
     * @param string $field
392
     * @param mixed  $default
393
     *
394
     * @return mixed
395
     */
396
    protected function mutateValue(array $mutators, string $field, $default)
397
    {
398
        //Let's process default value using associated setter
399
        if (isset($mutators[RecordEntity::MUTATOR_SETTER][$field])) {
400
            try {
401
                $setter = $mutators[RecordEntity::MUTATOR_SETTER][$field];
402
                $default = call_user_func($setter, $default);
403
404
                return $default;
405
            } catch (\Exception $exception) {
406
                //Unable to generate default value, use null or empty array as fallback
407
            }
408
        }
409
410
        if (isset($mutators[RecordEntity::MUTATOR_ACCESSOR][$field])) {
411
            $default = $this->accessorDefault(
412
                $default,
413
                $mutators[RecordEntity::MUTATOR_ACCESSOR][$field]
414
            );
415
416
            return $default;
417
        }
418
419
        return $default;
420
    }
421
422
    /**
423
     * Pass value thought accessor to ensure it's default.
424
     *
425
     * @param mixed  $default
426
     * @param string $accessor
427
     *
428
     * @return mixed
429
     *
430
     * @throws AccessException
431
     */
432
    protected function accessorDefault($default, string $accessor)
433
    {
434
        /**
435
         * @var AccessorInterface $instance
436
         */
437
        $instance = new $accessor($default, [/*no context given*/]);
438
        $default = $instance->packValue();
439
440
        if (is_object($default)) {
441
            //Some accessors might want to return objects (DateTime, StorageObject), default to null
442
            $default = null;
443
        }
444
445
        return $default;
446
    }
447
}