1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\DB; |
4
|
|
|
|
5
|
|
|
use Helix\DB; |
6
|
|
|
use ReflectionClass; |
7
|
|
|
use ReflectionNamedType; |
8
|
|
|
use ReflectionProperty; |
9
|
|
|
|
10
|
|
|
/** |
11
|
|
|
* Interprets classes and annotations. |
12
|
|
|
* |
13
|
|
|
* > Programmer's Note: Manipulating subclasses through a parent's reflection is allowed. |
14
|
|
|
* |
15
|
|
|
* @method static static factory(DB $db, string|object $class) |
16
|
|
|
*/ |
17
|
|
|
class Reflection |
18
|
|
|
{ |
19
|
|
|
|
20
|
|
|
use FactoryTrait; |
21
|
|
|
|
22
|
|
|
protected const RX_RECORD = '/\*\h*@record\h+(?<table>\w+)/i'; |
23
|
|
|
protected const RX_IS_COLUMN = '/\*\h*@col(umn)?\b/i'; |
24
|
|
|
protected const RX_UNIQUE = '/\*\h*@unique(\h+(?<ident>[a-z_]+))?/i'; |
25
|
|
|
protected const RX_VAR = '/\*\h*@var\h+(?<type>\S+)/i'; // includes pipes and backslashes |
26
|
|
|
protected const RX_NULL = '/(\bnull\|)|(\|null\b)/i'; |
27
|
|
|
protected const RX_EAV = '/\*\h*@eav\h+(?<table>\w+)/i'; |
28
|
|
|
protected const RX_EAV_VAR = '/\*\h*@var\h+(?<type>\w+)\[\]/i'; // typed array |
29
|
|
|
protected const RX_JUNCTION = '/\*\h*@junction\h+(?<table>\w+)/i'; |
30
|
|
|
protected const RX_FOREIGN = '/\*\h*@foreign\h+(?<column>\w+)\h+(?<class>\S+)/i'; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Maps annotated/native scalar types to storage types acceptable for `settype()` |
34
|
|
|
* |
35
|
|
|
* @see Schema::T_CONST_NAMES keys |
36
|
|
|
*/ |
37
|
|
|
const SCALARS = [ |
38
|
|
|
'bool' => 'bool', |
39
|
|
|
'boolean' => 'bool', // gettype() |
40
|
|
|
'double' => 'float', // gettype() |
41
|
|
|
'false' => 'bool', // @var |
42
|
|
|
'float' => 'float', |
43
|
|
|
'int' => 'int', |
44
|
|
|
'integer' => 'int', // gettype() |
45
|
|
|
'NULL' => 'string', // gettype() |
46
|
|
|
'number' => 'string', // @var |
47
|
|
|
'scalar' => 'string', // @var |
48
|
|
|
'string' => 'string', |
49
|
|
|
'String' => 'String', // @var |
50
|
|
|
'STRING' => 'STRING', // @var |
51
|
|
|
'true' => 'bool', // @var |
52
|
|
|
]; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var ReflectionClass |
56
|
|
|
*/ |
57
|
|
|
protected $class; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var DB |
61
|
|
|
*/ |
62
|
|
|
protected $db; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var array |
66
|
|
|
*/ |
67
|
|
|
protected $defaults = []; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var ReflectionProperty[] |
71
|
|
|
*/ |
72
|
|
|
protected $props = []; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @param DB $db |
76
|
|
|
* @param string|object $class |
77
|
|
|
*/ |
78
|
|
|
public function __construct(DB $db, $class) |
79
|
|
|
{ |
80
|
|
|
$this->db = $db; |
81
|
|
|
$this->class = new ReflectionClass($class); |
82
|
|
|
$this->defaults = $this->class->getDefaultProperties(); |
83
|
|
|
foreach ($this->class->getProperties() as $prop) { |
84
|
|
|
$prop->setAccessible(true); |
85
|
|
|
$this->props[$prop->getName()] = $prop; |
86
|
|
|
} |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @TODO allow aliasing |
91
|
|
|
* |
92
|
|
|
* @return string[] |
93
|
|
|
*/ |
94
|
|
|
public function getColumns(): array |
95
|
|
|
{ |
96
|
|
|
$cols = []; |
97
|
|
|
foreach ($this->props as $name => $prop) { |
98
|
|
|
if (preg_match(static::RX_IS_COLUMN, $prop->getDocComment())) { |
99
|
|
|
$cols[$name] = $name; |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
return $cols; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @return EAV[] |
107
|
|
|
*/ |
108
|
|
|
public function getEav() |
109
|
|
|
{ |
110
|
|
|
$EAV = []; |
111
|
|
|
foreach ($this->props as $name => $prop) { |
112
|
|
|
$doc = $prop->getDocComment(); |
113
|
|
|
if (preg_match(static::RX_EAV, $doc, $eav)) { |
114
|
|
|
preg_match(static::RX_EAV_VAR, $doc, $var); |
115
|
|
|
$type = $var['type'] ?? 'string'; |
116
|
|
|
$type = static::SCALARS[$type] ?? 'string'; |
117
|
|
|
$EAV[$name] = EAV::factory($this->db, $eav['table'], $type); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
return $EAV; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Classes keyed by column. |
125
|
|
|
* |
126
|
|
|
* @return string[] |
127
|
|
|
*/ |
128
|
|
|
public function getForeignClasses(): array |
129
|
|
|
{ |
130
|
|
|
preg_match_all(static::RX_FOREIGN, $this->class->getDocComment(), $foreign, PREG_SET_ORDER); |
131
|
|
|
return array_column($foreign, 'class', 'column'); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @return string |
136
|
|
|
*/ |
137
|
|
|
public function getJunctionTable(): string |
138
|
|
|
{ |
139
|
|
|
preg_match(static::RX_JUNCTION, $this->class->getDocComment(), $junction); |
140
|
|
|
return $junction['table']; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* @return string |
145
|
|
|
*/ |
146
|
|
|
public function getRecordTable(): string |
147
|
|
|
{ |
148
|
|
|
preg_match(static::RX_RECORD, $this->class->getDocComment(), $record); |
149
|
|
|
return $record['table']; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Scalar storage type, or FQN of a complex type. |
154
|
|
|
* |
155
|
|
|
* @param string $column |
156
|
|
|
* @return string |
157
|
|
|
*/ |
158
|
|
|
public function getType(string $column): string |
159
|
|
|
{ |
160
|
|
|
return $this->getType_reflection($column) |
161
|
|
|
?? $this->getType_var($column) |
162
|
|
|
?? static::SCALARS[gettype($this->defaults[$column])]; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @param string $column |
167
|
|
|
* @return null|string |
168
|
|
|
*/ |
169
|
|
|
protected function getType_reflection(string $column): ?string |
170
|
|
|
{ |
171
|
|
|
if ($type = $this->props[$column]->getType() and $type instanceof ReflectionNamedType) { |
172
|
|
|
return static::SCALARS[$type->getName()] ?? $type->getName(); |
173
|
|
|
} |
174
|
|
|
return null; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* @param string $column |
179
|
|
|
* @return null|string |
180
|
|
|
*/ |
181
|
|
|
protected function getType_var(string $column): ?string |
182
|
|
|
{ |
183
|
|
|
if (preg_match(static::RX_VAR, $this->props[$column]->getDocComment(), $var)) { |
184
|
|
|
$type = preg_replace(static::RX_NULL, '', $var['type']); // remove null |
185
|
|
|
if (isset(static::SCALARS[$type])) { |
186
|
|
|
return static::SCALARS[$type]; |
187
|
|
|
} |
188
|
|
|
// it's beyond the scope of this class to parse "use" statements (for now), |
189
|
|
|
// @var <CLASS> must be a FQN in order to work. |
190
|
|
|
return ltrim($type, '\\'); |
191
|
|
|
} |
192
|
|
|
return null; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Column groupings for unique constraints. |
197
|
|
|
* - Column-level constraints are enumerated names. |
198
|
|
|
* - Table-level (multi-column) constraints are names grouped under an arbitrary shared identifier. |
199
|
|
|
* |
200
|
|
|
* `[ 'foo', 'my_multi'=>['bar','baz'], ... ]` |
201
|
|
|
* |
202
|
|
|
* @return array |
203
|
|
|
*/ |
204
|
|
|
public function getUnique() |
205
|
|
|
{ |
206
|
|
|
$unique = []; |
207
|
|
|
foreach ($this->props as $prop) { |
208
|
|
|
if (preg_match(static::RX_UNIQUE, $prop->getDocComment(), $match)) { |
209
|
|
|
if (isset($match['ident'])) { |
210
|
|
|
$unique[$match['ident']][] = $prop->getName(); |
211
|
|
|
} else { |
212
|
|
|
$unique[] = $prop->getName(); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
return $unique; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* @param object $object |
221
|
|
|
* @param string $prop |
222
|
|
|
* @return mixed |
223
|
|
|
*/ |
224
|
|
|
public function getValue(object $object, string $prop) |
225
|
|
|
{ |
226
|
|
|
return $this->props[$prop]->getValue($object); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* @param string $column |
231
|
|
|
* @return bool |
232
|
|
|
*/ |
233
|
|
|
public function isNullable(string $column): bool |
234
|
|
|
{ |
235
|
|
|
if ($type = $this->props[$column]->getType()) { |
236
|
|
|
return $type->allowsNull(); |
237
|
|
|
} |
238
|
|
|
if (preg_match(static::RX_VAR, $this->props[$column]->getDocComment(), $var)) { |
239
|
|
|
return (bool)preg_match(static::RX_NULL, $var['type']); |
240
|
|
|
} |
241
|
|
|
return $this->defaults[$column] === null; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* @return object |
246
|
|
|
*/ |
247
|
|
|
public function newProto(): object |
248
|
|
|
{ |
249
|
|
|
return $this->class->newInstanceWithoutConstructor(); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* @param object $object |
254
|
|
|
* @param string $prop |
255
|
|
|
* @param mixed $value |
256
|
|
|
*/ |
257
|
|
|
public function setValue(object $object, string $prop, $value) |
258
|
|
|
{ |
259
|
|
|
$this->props[$prop]->setValue($object, $value); |
260
|
|
|
} |
261
|
|
|
} |
262
|
|
|
|