1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Spiral, Core Components |
4
|
|
|
* |
5
|
|
|
* @author Wolfy-J |
6
|
|
|
*/ |
7
|
|
|
|
8
|
|
|
namespace Spiral\ORM\Helpers; |
9
|
|
|
|
10
|
|
|
use Spiral\Database\Schemas\Prototypes\AbstractColumn; |
11
|
|
|
use Spiral\Database\Schemas\Prototypes\AbstractTable; |
12
|
|
|
use Spiral\ORM\ColumnInterface; |
13
|
|
|
use Spiral\ORM\Exceptions\ColumnRenderException; |
14
|
|
|
use Spiral\ORM\Exceptions\DefinitionException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Implements ability to define column in AbstractSchema based on string representation and default |
18
|
|
|
* value (if defined). |
19
|
|
|
* |
20
|
|
|
* Attention, this class will try to guess default value if column is NOT NULL and no default |
21
|
|
|
* value provided by user. |
22
|
|
|
*/ |
23
|
|
|
class ColumnRenderer |
24
|
|
|
{ |
25
|
|
|
/** |
26
|
|
|
* Render columns in table based on string definition. |
27
|
|
|
* |
28
|
|
|
* Example: |
29
|
|
|
* renderColumns([ |
30
|
|
|
* 'id' => 'primary', |
31
|
|
|
* 'time' => 'datetime, nullable', |
32
|
|
|
* 'status' => 'enum(active, disabled)' |
33
|
|
|
* ], [ |
34
|
|
|
* 'status' => 'active', |
35
|
|
|
* 'time' => null, //idential as if define column as null |
36
|
|
|
* ], $table); |
37
|
|
|
* |
38
|
|
|
* Attention, new table instance will be returned! |
39
|
|
|
* |
40
|
|
|
* @param array $fields |
41
|
|
|
* @param array $defaults |
42
|
|
|
* @param AbstractTable $table |
43
|
|
|
* |
44
|
|
|
* @return AbstractTable |
45
|
|
|
* |
46
|
|
|
* @throws ColumnRenderException |
47
|
|
|
*/ |
48
|
|
|
public function renderColumns( |
49
|
|
|
array $fields, |
50
|
|
|
array $defaults, |
51
|
|
|
AbstractTable $table |
52
|
|
|
): AbstractTable { |
53
|
|
|
foreach ($fields as $name => $definition) { |
54
|
|
|
$column = $table->column($name); |
55
|
|
|
|
56
|
|
|
//Declaring our column |
57
|
|
|
$this->declareColumn( |
58
|
|
|
$column, |
59
|
|
|
$definition, |
60
|
|
|
array_key_exists($name, $defaults), |
61
|
|
|
$defaults[$name] ?? null |
62
|
|
|
); |
63
|
|
|
} |
64
|
|
|
|
65
|
|
|
return clone $table; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* Cast (specify) column schema based on provided column definition and default value. |
70
|
|
|
* Spiral will force default values (internally) for every NOT NULL column except primary keys! |
71
|
|
|
* |
72
|
|
|
* Column definition are compatible with database Migrations and AbstractColumn types. |
73
|
|
|
* |
74
|
|
|
* Column definition examples (by default all columns has flag NOT NULL): |
75
|
|
|
* protected $schema = [ |
76
|
|
|
* 'id' => 'primary', |
77
|
|
|
* 'name' => 'string', //Default length is 255 characters. |
78
|
|
|
* 'email' => 'string(255), nullable', //Can be NULL |
79
|
|
|
* 'status' => 'enum(active, pending, disabled)', //Enum values, trimmed |
80
|
|
|
* 'balance' => 'decimal(10, 2)', |
81
|
|
|
* 'message' => 'text, null', //Alias for nullable |
82
|
|
|
* 'time_expired' => 'timestamp' |
83
|
|
|
* ]; |
84
|
|
|
* |
85
|
|
|
* Attention, column state will be affected! |
86
|
|
|
* |
87
|
|
|
* @see AbstractColumn |
88
|
|
|
* |
89
|
|
|
* @todo convert column type exception into definition |
90
|
|
|
* |
91
|
|
|
* @param AbstractColumn $column |
92
|
|
|
* @param string $definition |
93
|
|
|
* @param bool $hasDefault Must be set to true if default value was set by user. |
94
|
|
|
* @param mixed $default Default value declared by record schema. |
95
|
|
|
* |
96
|
|
|
* @return mixed |
97
|
|
|
* |
98
|
|
|
* @throws DefinitionException |
99
|
|
|
*/ |
100
|
|
|
public function declareColumn( |
101
|
|
|
AbstractColumn $column, |
102
|
|
|
string $definition, |
103
|
|
|
bool $hasDefault, |
104
|
|
|
$default = null |
105
|
|
|
) { |
106
|
|
|
if ( |
107
|
|
|
class_exists($definition) |
108
|
|
|
&& is_a($definition, ColumnInterface::class, true) |
109
|
|
|
) { |
110
|
|
|
//Dedicating column definition to our column class |
111
|
|
|
call_user_func([$definition, 'describeColumn'], $column); |
112
|
|
|
|
113
|
|
|
return $column; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
//Expression used to declare column type, easy to read |
117
|
|
|
$pattern = '/(?P<type>[a-z]+)(?: *\((?P<options>[^\)]+)\))?(?: *, *(?P<nullable>null(?:able)?))?/i'; |
118
|
|
|
|
119
|
|
|
if (!preg_match($pattern, $definition, $type)) { |
120
|
|
|
throw new DefinitionException( |
121
|
|
|
"Invalid column type definition in '{$column->getTable()}'.'{$column->getName()}'" |
122
|
|
|
); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
if (!empty($type['options'])) { |
126
|
|
|
//Exporting and trimming |
127
|
|
|
$type['options'] = array_map('trim', explode(',', $type['options'])); |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
//ORM force EVERY column to NOT NULL state unless different is said |
131
|
|
|
$column->nullable(false); |
132
|
|
|
|
133
|
|
|
if (!empty($type['nullable'])) { |
134
|
|
|
//Indication that column is nullable |
135
|
|
|
$column->nullable(true); |
136
|
|
|
} |
137
|
|
|
try { |
138
|
|
|
//Bypassing call to AbstractColumn->__call method (or specialized column method) |
139
|
|
|
call_user_func_array( |
140
|
|
|
[$column, $type['type']], |
141
|
|
|
!empty($type['options']) ? $type['options'] : [] |
142
|
|
|
); |
143
|
|
|
} catch (\Throwable $e) { |
144
|
|
|
throw new DefinitionException( |
145
|
|
|
"Invalid column type definition in '{$column->getTable()}'.'{$column->getName()}'", |
146
|
|
|
$e->getCode(), |
147
|
|
|
$e |
148
|
|
|
); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
if (in_array($column->abstractType(), ['primary', 'bigPrimary'])) { |
152
|
|
|
//No default value can be set of primary keys |
153
|
|
|
return $column; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
if (!$hasDefault && !$column->isNullable()) { |
157
|
|
|
//We have to come up with some default value |
158
|
|
|
return $column->defaultValue($this->castDefault($column)); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
if (is_null($default)) { |
162
|
|
|
//Default value is stated and NULL, clear what to do |
163
|
|
|
$column->nullable(true); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return $column->defaultValue($default); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
/** |
170
|
|
|
* Cast default value based on column type. Required to prevent conflicts when not nullable |
171
|
|
|
* column added to existed table with data in. |
172
|
|
|
* |
173
|
|
|
* @param AbstractColumn $column |
174
|
|
|
* |
175
|
|
|
* @return mixed |
176
|
|
|
*/ |
177
|
|
|
public function castDefault(AbstractColumn $column) |
178
|
|
|
{ |
179
|
|
|
if (in_array($column->abstractType(), ['timestamp', 'datetime', 'time', 'date'])) { |
180
|
|
|
return 0; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if ($column->abstractType() == 'enum') { |
184
|
|
|
//We can use first enum value as default |
185
|
|
|
return $column->getEnumValues()[0]; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
switch ($column->phpType()) { |
189
|
|
|
case 'int': |
|
|
|
|
190
|
|
|
return 0; |
191
|
|
|
case 'float': |
|
|
|
|
192
|
|
|
return 0.0; |
193
|
|
|
case 'bool': |
194
|
|
|
return false; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
return ''; |
198
|
|
|
} |
199
|
|
|
} |
As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next
break
.There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.
To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.