Completed
Branch feature/pre-split (d4e072)
by Anton
04:00
created

RecordSchema::packSchema()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 16
nc 2
nop 3
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
1
<?php
2
/**
3
 * components
4
 *
5
 * @author    Wolfy-J
6
 */
7
namespace Spiral\ORM\Schemas;
8
9
use Doctrine\Common\Inflector\Inflector;
10
use Spiral\Database\Schemas\Prototypes\AbstractTable;
11
use Spiral\Models\AccessorInterface;
12
use Spiral\Models\Exceptions\AccessorExceptionInterface;
13
use Spiral\Models\Reflections\ReflectionEntity;
14
use Spiral\ORM\Configs\MutatorsConfig;
15
use Spiral\ORM\Entities\RecordInstantiator;
16
use Spiral\ORM\Exceptions\DefinitionException;
17
use Spiral\ORM\Helpers\ColumnRenderer;
18
use Spiral\ORM\ORMInterface;
19
use Spiral\ORM\Record;
20
use Spiral\ORM\RecordEntity;
21
use Spiral\ORM\Schemas\Definitions\IndexDefinition;
22
use Spiral\ORM\Schemas\Definitions\RelationDefinition;
23
24
class RecordSchema implements SchemaInterface
25
{
26
    /**
27
     * @var ReflectionEntity
28
     */
29
    private $reflection;
30
31
    /**
32
     * @invisible
33
     * @var MutatorsConfig
34
     */
35
    private $mutatorsConfig;
36
37
    /**
38
     * @invisible
39
     * @var ColumnRenderer
40
     */
41
    private $renderer;
42
43
    /**
44
     * @param ReflectionEntity    $reflection
45
     * @param MutatorsConfig      $mutators
46
     * @param ColumnRenderer|null $rendered
47
     */
48
    public function __construct(
49
        ReflectionEntity $reflection,
50
        MutatorsConfig $mutators,
51
        ColumnRenderer $rendered = null
52
    ) {
53
        $this->reflection = $reflection;
54
        $this->mutatorsConfig = $mutators;
55
        $this->renderer = $rendered ?? new ColumnRenderer();
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function getClass(): string
62
    {
63
        return $this->reflection->getName();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getRole(): string
70
    {
71
        $role = $this->reflection->getProperty('model_role');
72
73
        //When role not defined we are going to use short class name
74
        return $role ?? lcfirst($this->reflection->getShortName());
75
    }
76
77
    /**
78
     * @return ReflectionEntity
79
     */
80
    public function getReflection(): ReflectionEntity
81
    {
82
        return $this->reflection;
83
    }
84
85
    /**
86
     * {@inheritdoc}
87
     */
88
    public function getInstantiator(): string
89
    {
90
        return $this->reflection->getProperty('instantiator') ?? RecordInstantiator::class;
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96
    public function getDatabase()
97
    {
98
        $database = $this->reflection->getProperty('database');
99
        if (empty($database)) {
100
            //Empty database to be used
101
            return null;
102
        }
103
104
        return $database;
105
    }
106
107
    /**
108
     * {@inheritdoc}
109
     */
110
    public function getTable(): string
111
    {
112
        $table = $this->reflection->getProperty('table');
113
        if (empty($table)) {
114
            //Generate collection using short class name
115
            $table = Inflector::camelize($this->reflection->getShortName());
116
            $table = Inflector::pluralize($table);
117
        }
118
119
        return $table;
120
    }
121
122
    /**
123
     * Fields and their types declared in Record model.
124
     *
125
     * @return array
126
     */
127
    public function getFields(): array
128
    {
129
        $fields = $this->reflection->getSchema();
130
131
        foreach ($fields as $field => $type) {
132
            if ($this->isRelation($type)) {
133
                unset($fields[$field]);
134
            }
135
        }
136
137
        return $fields;
138
    }
139
140
    /**
141
     * Returns set of declared indexes.
142
     *
143
     * Example:
144
     * const INDEXES = [
145
     *      [self::UNIQUE, 'email'],
146
     *      [self::INDEX, 'status', 'balance'],
147
     *      [self::INDEX, 'public_id']
148
     * ];
149
     *
150
     * @do generator
151
     *
152
     * @return \Generator|IndexDefinition[]
153
     *
154
     * @throws DefinitionException
155
     */
156
    public function getIndexes(): \Generator
157
    {
158
        $definitions = $this->reflection->getProperty('indexes') ?? [];
159
160
        foreach ($definitions as $definition) {
161
            yield $this->castIndex($definition);
162
        }
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168
    public function declareTable(AbstractTable $table): AbstractTable
169
    {
170
        return $this->renderer->renderColumns(
171
            $this->getFields(),
172
            $this->getDefaults(),
173
            $table
174
        );
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180
    public function getRelations(): \Generator
181
    {
182
        $schema = $this->reflection->getSchema();
183
184
        foreach ($schema as $name => $definition) {
185
            if (!$this->isRelation($definition)) {
186
                continue;
187
            }
188
189
            /**
190
             * We expect relations to be defined in a following form:
191
             *
192
             * [type => target, option => value, option => value]
193
             */
194
            $type = key($definition);
195
            $target = $definition[$type];
196
            unset($definition[$type]);
197
198
            //Defining relation
199
            yield new RelationDefinition(
200
                $name,
201
                $type,
202
                $target,
203
                $definition,
204
                $definition[Record::INVERSE] ?? null
205
            );
206
        }
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212
    public function packSchema(
213
        SchemaBuilder $builder,
214
        AbstractTable $table,
215
        array $packedRelations = []
216
    ): array {
217
218
        $relations = [];
219
        foreach ($packedRelations as $relation => $schema) {
220
            $relations[$relation] = $schema[ORMInterface::R_TYPE];
221
        }
222
223
        return [
224
            RecordEntity::SH_PRIMARIES => $table->getPrimaryKeys(),
225
226
            //Default entity values
227
            RecordEntity::SH_DEFAULTS  => $this->packDefaults($table),
228
229
            //Entity behaviour
230
            RecordEntity::SH_HIDDEN    => $this->reflection->getHidden(),
231
            RecordEntity::SH_SECURED   => $this->reflection->getSecured(),
232
            RecordEntity::SH_FILLABLE  => $this->reflection->getFillable(),
233
234
            //Mutators can be altered based on ORM\SchemasConfig
235
            RecordEntity::SH_MUTATORS  => $this->buildMutators($table),
236
237
            RecordEntity::SH_RELATIONS => $relations
238
        ];
239
    }
240
241
    /**
242
     * Generate set of default values to be used by record.
243
     *
244
     * @param AbstractTable $table
245
     *
246
     * @return array
247
     */
248
    protected function packDefaults(AbstractTable $table): array
249
    {
250
        //We need mutators to normalize default values
251
        $mutators = $this->buildMutators($table);
252
253
        $defaults = [];
254
        foreach ($table->getColumns() as $column) {
255
            $field = $column->getName();
256
257
            $default = $column->getDefaultValue();
258
259
            //For non null values let's apply mutators to typecast it
260
            if (!is_null($default) && !is_object($default) && !$column->isNullable()) {
261
                $default = $this->mutateValue($mutators, $field, $default);
262
            }
263
264
            $defaults[$field] = $default;
265
        }
266
267
        return $defaults;
268
    }
269
270
    /**
271
     * Generate set of mutators associated with entity fields using user defined and automatic
272
     * mutators.
273
     *
274
     * @see MutatorsConfig
275
     *
276
     * @param AbstractTable $table
277
     *
278
     * @return array
279
     */
280
    protected function buildMutators(AbstractTable $table): array
281
    {
282
        $mutators = $this->reflection->getMutators();
283
284
        //Trying to resolve mutators based on field type
285
        foreach ($table->getColumns() as $column) {
286
            //Resolved mutators
287
            $resolved = [];
288
289
            if (!empty($filter = $this->mutatorsConfig->getMutators($column->abstractType()))) {
290
                //Mutator associated with type directly
291
                $resolved += $filter;
292
            } elseif (!empty($filter = $this->mutatorsConfig->getMutators('php:' . $column->phpType()))) {
293
                //Mutator associated with php type
294
                $resolved += $filter;
295
            }
296
297
            //Merging mutators and default mutators
298
            foreach ($resolved as $mutator => $filter) {
299
                if (!array_key_exists($column->getName(), $mutators[$mutator])) {
300
                    $mutators[$mutator][$column->getName()] = $filter;
301
                }
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 AccessorExceptionInterface
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
}