Completed
Branch feature/pre-split (af0512)
by Anton
03:32
created

RecordSchema::declareTable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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