Completed
Branch feature/pre-split (220ee1)
by Anton
03:19
created

RecordSchema::getFields()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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