Passed
Push — master ( b87dcd...007324 )
by Anton
01:56
created

RenderTable::compute()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 6
nop 2
dl 0
loc 23
rs 9.8333
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
namespace Cycle\Schema\Visitor;
11
12
use Cycle\Schema\Builder;
13
use Cycle\Schema\Definition\Entity;
14
use Cycle\Schema\Exception\BuilderException;
15
use Cycle\Schema\VisitorInterface;
16
use Spiral\Database\Schema\AbstractColumn;
17
18
/**
19
 * Generate table columns based on entity definition.
20
 */
21
class RenderTable implements VisitorInterface
22
{
23
    /**
24
     * Generate table schema based on given entity definition.
25
     *
26
     * @param Builder $builder
27
     * @param Entity  $entity
28
     */
29
    public function compute(Builder $builder, Entity $entity)
30
    {
31
        // todo: readonly
32
        $table = $builder->getTable($entity);
33
34
        $primaryKeys = [];
35
        foreach ($entity->getFields() as $field) {
36
            $type = $this->parse($table->getName(), $field->getColumn(), $field->getType());
37
38
            if ($this->hasFlag($type, 'primary')) {
39
                $primaryKeys[] = $field->getColumn();
40
            }
41
42
            $this->renderColumn(
43
                $table->column($field->getColumn()),
44
                $type,
45
                !is_null($field->getDefault()),
46
                $field->getDefault()
47
            );
48
49
        }
50
        if (count($primaryKeys)) {
51
            $table->setPrimaryKeys($primaryKeys);
52
        }
53
    }
54
55
    /**
56
     * Cast (specify) column schema based on provided column definition and default value.
57
     * Spiral will force default values (internally) for every NOT NULL column except primary keys!
58
     *
59
     * Column definition are compatible with database Migrations and AbstractColumn types.
60
     *
61
     * Column definition examples (by default all columns has flag NOT NULL):
62
     * const SCHEMA = [
63
     *      'id'           => 'primary',
64
     *      'name'         => 'string',                          //Default length is 255 characters.
65
     *      'email'        => 'string(255), nullable',           //Can be NULL
66
     *      'status'       => 'enum(active, pending, disabled)', //Enum values, trimmed
67
     *      'balance'      => 'decimal(10, 2)',
68
     *      'message'      => 'text, null',                      //Alias for nullable
69
     *      'time_expired' => 'timestamp'
70
     * ];
71
     *
72
     * Attention, column state will be affected!
73
     *
74
     * @see  AbstractColumn
75
     * @param AbstractColumn $column
76
     * @param array          $type
77
     * @param bool           $hasDefault Must be set to true if default value was set by user.
78
     * @param mixed          $default    Default value declared by record schema.
79
     *
80
     * @throws BuilderException
81
     */
82
    protected function renderColumn(AbstractColumn $column, array $type, bool $hasDefault, $default = null)
83
    {
84
        // ORM force EVERY column to NOT NULL state unless different is said
85
        $column->nullable(false);
86
87
        if ($this->hasFlag($type, 'null') || $this->hasFlag($type, 'nullable')) {
88
            // indication that column is nullable
89
            $column->nullable(true);
90
        }
91
92
        try {
93
            // bypassing call to AbstractColumn->__call method (or specialized column method)
94
            call_user_func_array([$column, $type['type']], $type['options']);
95
        } catch (\Throwable $e) {
96
            throw new BuilderException(
97
                "Invalid column type definition in '{$column->getTable()}'.'{$column->getName()}'",
98
                $e->getCode(),
99
                $e
100
            );
101
        }
102
103
        if (in_array($column->getAbstractType(), ['primary', 'bigPrimary'])) {
104
            // no default value can be set of primary keys
105
            return;
106
        }
107
108
        if (!$hasDefault && !$column->isNullable()) {
109
            if (!$this->hasFlag($type, 'required') && !$this->hasFlag($type, 'primary')) {
110
                // we have to come up with some default value
111
                $column->defaultValue($this->castDefault($column));
112
            }
113
114
            return;
115
        }
116
117
        if (is_null($default)) {
118
            // default value is stated and NULL, clear what to do
119
            $column->nullable(true);
120
        }
121
122
        $column->defaultValue($default);
123
    }
124
125
    /**
126
     * @param string $table
127
     * @param string $column
128
     * @param string $definition
129
     * @return array
130
     */
131
    protected function parse(string $table, string $column, string $definition): array
132
    {
133
        if (!preg_match(
134
            '/(?P<type>[a-z]+)(?: *\((?P<options>[^\)]+)\))?(?: *, *(?P<flags>.+))?/i',
135
            $definition,
136
            $type
137
        )) {
138
            throw new BuilderException("Invalid column type definition in '{$table}'.'{$column}'");
139
        }
140
141
        if (empty($type['options'])) {
142
            $type['options'] = [];
143
        } else {
144
            $type['options'] = array_map('trim', explode(',', $type['options'] ?? ''));
145
        }
146
147
        if (empty($type['flags'])) {
148
            $type['flags'] = [];
149
        } else {
150
            $type['flags'] = array_map('trim', explode(',', $type['flags'] ?? ''));
151
        }
152
153
        unset($type[0], $type[1], $type[2], $type[3]);
154
155
        return $type;
156
    }
157
158
    /**
159
     * @param array  $type
160
     * @param string $flag
161
     * @return bool
162
     */
163
    protected function hasFlag(array $type, string $flag): bool
164
    {
165
        return in_array($flag, $type['flags'], true);
166
    }
167
168
    /**
169
     * Cast default value based on column type. Required to prevent conflicts when not nullable
170
     * column added to existed table with data in.
171
     *
172
     * @param AbstractColumn $column
173
     * @return mixed
174
     */
175
    protected function castDefault(AbstractColumn $column)
176
    {
177
        if (in_array($column->getAbstractType(), ['timestamp', 'datetime', 'time', 'date'])) {
178
            return 0;
179
        }
180
181
        if ($column->getAbstractType() == 'enum') {
182
            // we can use first enum value as default
183
            return $column->getEnumValues()[0];
184
        }
185
186
        switch ($column->getType()) {
187
            case AbstractColumn::INT:
188
                return 0;
189
            case AbstractColumn::FLOAT:
190
                return 0.0;
191
            case AbstractColumn::BOOL:
192
                return false;
193
        }
194
195
        return '';
196
    }
197
}