Passed
Push — master ( ae25c0...fd0368 )
by Anton
01:32
created

ColumnSchema::render()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 4
nop 1
dl 0
loc 23
rs 9.5222
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\Generator\Table;
11
12
use Cycle\Schema\Definition\Field;
13
use Cycle\Schema\Exception\ColumnException;
14
use Spiral\Database\Schema\AbstractColumn;
15
16
/**
17
 * Carries information about column definition.
18
 *
19
 * @internal
20
 */
21
final class ColumnSchema
22
{
23
    // indicates that column must be treated as primary
24
    public const OPT_PRIMARY = 'primary';
25
26
    // default column value
27
    public const OPT_DEFAULT = 'default';
28
29
    // column can automatically define default value
30
    public const OPT_CAST_DEFAULT = 'castDefault';
31
32
    // column can be nullable
33
    public const OPT_NULLABLE = 'nullable';
34
35
    // provides ability to define complex types using string notation, i.e. string(32)
36
    private const DEFINITION = '/(?P<type>[a-z]+)(?: *\((?P<options>[^\)]+)\))?/i';
37
38
    /** @var Field */
39
    private $field;
40
41
    /** @var string */
42
    private $type;
43
44
    /** @var array */
45
    private $typeOptions = [];
46
47
    /**
48
     * @return string
49
     */
50
    public function getName(): string
51
    {
52
        return $this->field->getColumn();
53
    }
54
55
    /**
56
     * Get column type.
57
     *
58
     * @return string
59
     */
60
    public function getType(): string
61
    {
62
        return $this->type;
63
    }
64
65
    /**
66
     * @return bool
67
     */
68
    public function isPrimary(): bool
69
    {
70
        return in_array($this->type, ['primary', 'bigPrimary']) || $this->hasOption(self::OPT_PRIMARY);
71
    }
72
73
    /**
74
     * @return bool
75
     */
76
    public function isNullable(): bool
77
    {
78
        if ($this->hasDefault() && $this->getDefault() === null) {
79
            return true;
80
        }
81
82
        return $this->hasOption(self::OPT_NULLABLE) && !$this->isPrimary();
83
    }
84
85
    /**
86
     * @return bool
87
     */
88
    public function hasDefault(): bool
89
    {
90
        if ($this->isPrimary()) {
91
            return false;
92
        }
93
94
        return $this->hasOption(self::OPT_DEFAULT);
95
    }
96
97
    /**
98
     * @return mixed
99
     *
100
     * @throws ColumnException
101
     */
102
    public function getDefault()
103
    {
104
        if (!$this->hasDefault()) {
105
            throw new ColumnException("No default value on `{$this->field->getColumn()}`");
106
        }
107
108
        return $this->field->getOptions()->get(self::OPT_DEFAULT);
109
    }
110
111
    /**
112
     * Render column definition.
113
     *
114
     * @param AbstractColumn $column
115
     *
116
     * @throws ColumnException
117
     */
118
    public function render(AbstractColumn $column)
119
    {
120
        $column->nullable($this->isNullable());
121
122
        try {
123
            // bypassing call to AbstractColumn->__call method (or specialized column method)
124
            call_user_func_array([$column, $this->type], $this->typeOptions);
125
        } catch (\Throwable $e) {
126
            throw new ColumnException(
127
                "Invalid column type definition in '{$column->getTable()}'.'{$column->getName()}'",
128
                $e->getCode(),
129
                $e
130
            );
131
        }
132
133
        if ($this->hasDefault() && $this->getDefault() !== null) {
134
            $column->defaultValue($this->getDefault());
135
            return;
136
        }
137
138
        if ($this->hasOption(self::OPT_CAST_DEFAULT)) {
139
            // cast default value
140
            $column->defaultValue($this->castDefault($column));
141
        }
142
    }
143
144
    /**
145
     * @param AbstractColumn $column
146
     * @return bool|float|int|string
147
     */
148
    private function castDefault(AbstractColumn $column)
149
    {
150
        if (in_array($column->getAbstractType(), ['timestamp', 'datetime', 'time', 'date'])) {
151
            return 0;
152
        }
153
154
        if ($column->getAbstractType() == 'enum') {
155
            // we can use first enum value as default
156
            return $column->getEnumValues()[0];
157
        }
158
159
        switch ($column->getType()) {
160
            case AbstractColumn::INT:
161
                return 0;
162
            case AbstractColumn::FLOAT:
163
                return 0.0;
164
            case AbstractColumn::BOOL:
165
                return false;
166
        }
167
168
        return '';
169
    }
170
171
    /**
172
     * @param string $option
173
     * @return bool
174
     */
175
    private function hasOption(string $option): bool
176
    {
177
        return $this->field->getOptions()->has($option);
178
    }
179
180
    /**
181
     * Parse field definition into table definition.
182
     *
183
     * @param Field $field
184
     * @return ColumnSchema
185
     *
186
     * @throws ColumnException
187
     */
188
    public static function parse(Field $field): self
189
    {
190
        $column = new ColumnSchema();
191
        $column->field = $field;
192
193
        if (!preg_match(self::DEFINITION, $field->getType(), $type)) {
194
            throw new ColumnException("Invalid column type definition in `{$field->getType()}`");
195
        }
196
197
        $column->type = $type['type'];
198
        if (!empty($type['options'])) {
199
            $column->typeOptions = array_map('trim', explode(',', $type['options'] ?? ''));
200
        }
201
202
        return $column;
203
    }
204
}