Completed
Push — master ( e12b56...1fb17f )
by Anton
01:36
created

RenderTable   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 63
dl 0
loc 179
rs 10
c 0
b 0
f 0
wmc 27

5 Methods

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