Passed
Push — master ( 9cf790...f45573 )
by y
06:06
created

Reflection::isColumn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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, and manipulates objects.
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
 * @TODO Allow column aliasing.
18
 */
19
class Reflection
20
{
21
22
    use FactoryTrait;
23
24
    protected const RX_RECORD = '/\*\h*@record\h+(?<table>\w+)/i';
25
    protected const RX_IS_COLUMN = '/\*\h*@col(umn)?\b/i';
26
    protected const RX_UNIQUE = '/^\h*\*\h*@unique\h*$/im';
27
    protected const RX_UNIQUE_GROUP = '/^\h*\*\h*@unique\h+(?<ident>[a-z_]+)\h*$/im';
28
    protected const RX_VAR = '/\*\h*@var\h+(?<type>\S+)/i'; // includes pipes and backslashes
29
    protected const RX_NULL = '/(\bnull\|)|(\|null\b)/i';
30
    protected const RX_EAV = '/\*\h*@eav\h+(?<table>\w+)/i';
31
    protected const RX_EAV_VAR = '/\*\h*@var\h+(?<type>\w+)\[\]/i'; // typed array
32
    protected const RX_JUNCTION = '/\*\h*@junction\h+(?<table>\w+)/i';
33
    protected const RX_FOREIGN = '/\*\h*@foreign\h+(?<column>\w+)\h+(?<class>\S+)/i';
34
35
    /**
36
     * Maps annotated/native scalar types to storage types acceptable for `settype()`
37
     *
38
     * @see Schema::T_CONST_NAMES keys
39
     */
40
    const SCALARS = [
41
        'bool' => 'bool',
42
        'boolean' => 'bool',    // gettype()
43
        'double' => 'float',    // gettype()
44
        'false' => 'bool',      // @var
45
        'float' => 'float',
46
        'int' => 'int',
47
        'integer' => 'int',     // gettype()
48
        'NULL' => 'string',     // gettype()
49
        'number' => 'string',   // @var
50
        'scalar' => 'string',   // @var
51
        'string' => 'string',
52
        'String' => 'String',   // @var
53
        'STRING' => 'STRING',   // @var
54
        'true' => 'bool',       // @var
55
    ];
56
57
    /**
58
     * @var ReflectionClass
59
     */
60
    protected $class;
61
62
    /**
63
     * @var string[]
64
     */
65
    protected $columns;
66
67
    /**
68
     * @var DB
69
     */
70
    protected $db;
71
72
    /**
73
     * @var array
74
     */
75
    protected $defaults = [];
76
77
    /**
78
     * @var ReflectionProperty[]
79
     */
80
    protected $properties = [];
81
82
    /**
83
     * @param DB $db
84
     * @param string|object $class
85
     */
86
    public function __construct(DB $db, $class)
87
    {
88
        $this->db = $db;
89
        $this->class = new ReflectionClass($class);
90
        $this->defaults = $this->class->getDefaultProperties();
91
        foreach ($this->class->getProperties() as $property) {
92
            $name = $property->getName();
93
            $property->setAccessible(true);
94
            $this->properties[$name] = $property;
95
            if ($this->isColumn($name)) {
96
                $this->columns[$name] = $name;
97
            }
98
        }
99
    }
100
101
    /**
102
     * @return string[]
103
     */
104
    final public function getColumns(): array
105
    {
106
        return $this->columns;
107
    }
108
109
    /**
110
     * @return EAV[]
111
     */
112
    public function getEav()
113
    {
114
        $EAV = [];
115
        foreach ($this->properties as $name => $prop) {
116
            $doc = $prop->getDocComment();
117
            if (preg_match(static::RX_EAV, $doc, $eav)) {
118
                preg_match(static::RX_EAV_VAR, $doc, $var);
119
                $type = $var['type'] ?? 'string';
120
                $type = static::SCALARS[$type] ?? 'string';
121
                $EAV[$name] = EAV::factory($this->db, $eav['table'], $type);
122
            }
123
        }
124
        return $EAV;
125
    }
126
127
    /**
128
     * Classes keyed by column.
129
     *
130
     * @return string[]
131
     */
132
    public function getForeignClasses(): array
133
    {
134
        preg_match_all(static::RX_FOREIGN, $this->class->getDocComment(), $foreign, PREG_SET_ORDER);
135
        return array_column($foreign, 'class', 'column');
136
    }
137
138
    /**
139
     * @return string
140
     */
141
    public function getJunctionTable(): string
142
    {
143
        preg_match(static::RX_JUNCTION, $this->class->getDocComment(), $junction);
144
        return $junction['table'];
145
    }
146
147
    /**
148
     * @return string
149
     */
150
    public function getRecordTable(): string
151
    {
152
        preg_match(static::RX_RECORD, $this->class->getDocComment(), $record);
153
        return $record['table'];
154
    }
155
156
    /**
157
     * Scalar storage type, or FQN of a complex type.
158
     *
159
     * @param string $property
160
     * @return string
161
     */
162
    public function getType(string $property): string
163
    {
164
        return $this->getType_reflection($property)
165
            ?? $this->getType_var($property)
166
            ?? static::SCALARS[gettype($this->defaults[$property])];
167
    }
168
169
    /**
170
     * @param string $property
171
     * @return null|string
172
     */
173
    protected function getType_reflection(string $property): ?string
174
    {
175
        if ($type = $this->properties[$property]->getType() and $type instanceof ReflectionNamedType) {
176
            return static::SCALARS[$type->getName()] ?? $type->getName();
177
        }
178
        return null;
179
    }
180
181
    /**
182
     * @param string $property
183
     * @return null|string
184
     */
185
    protected function getType_var(string $property): ?string
186
    {
187
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
188
            $type = preg_replace(static::RX_NULL, '', $var['type']); // remove null
189
            if (isset(static::SCALARS[$type])) {
190
                return static::SCALARS[$type];
191
            }
192
            // it's beyond the scope of this class to parse "use" statements (for now),
193
            // @var <CLASS> must be a FQN in order to work.
194
            return ltrim($type, '\\');
195
        }
196
        return null;
197
    }
198
199
    /**
200
     * Columns with their own individual unique constraints.
201
     *
202
     * @return string[]
203
     */
204
    public function getUnique(): array
205
    {
206
        return array_filter($this->columns, fn(string $property) => $this->isUnique($property));
207
    }
208
209
    /**
210
     * Table-level (multi-column) constraints are grouped under an arbitrary shared identifier.
211
     *
212
     * `[ shared identifier => [ col, ... ], ... ]`
213
     *
214
     * @return string[][]
215
     */
216
    public function getUniqueGroups(): array
217
    {
218
        $groups = [];
219
        foreach ($this->columns as $column) {
220
            if ($this->isUniqueMulti($column, $ident)) {
221
                $groups[$ident][$column] = $column;
222
            }
223
        }
224
        return $groups;
225
    }
226
227
    /**
228
     * Gets and returns a value through reflection.
229
     *
230
     * @param object $object
231
     * @param string $property
232
     * @return mixed
233
     */
234
    public function getValue(object $object, string $property)
235
    {
236
        return $this->properties[$property]->getValue($object);
237
    }
238
239
    /**
240
     * @param string $property
241
     * @return bool
242
     */
243
    public function isColumn(string $property): bool
244
    {
245
        return (bool)preg_match(static::RX_IS_COLUMN, $this->properties[$property]->getDocComment());
246
    }
247
248
    /**
249
     * Whether a property allows `NULL` as a value.
250
     *
251
     * @param string $property
252
     * @return bool
253
     */
254
    public function isNullable(string $property): bool
255
    {
256
        if ($type = $this->properties[$property]->getType()) {
257
            return $type->allowsNull();
258
        }
259
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
260
            return (bool)preg_match(static::RX_NULL, $var['type']);
261
        }
262
        return $this->defaults[$property] === null;
263
    }
264
265
    /**
266
     * @param string $column
267
     * @return bool
268
     */
269
    public function isUnique(string $column): bool
270
    {
271
        return (bool)preg_match(static::RX_UNIQUE, $this->properties[$column]->getDocComment());
272
    }
273
274
    /**
275
     * @param string $column
276
     * @param void $ident Updated to the group identifier.
277
     * @return bool
278
     */
279
    public function isUniqueMulti(string $column, &$ident = null): bool
280
    {
281
        if (preg_match(static::RX_UNIQUE_GROUP, $this->properties[$column]->getDocComment(), $unique)) {
282
            $ident = $unique['ident'];
283
            return true;
284
        }
285
        return false;
286
    }
287
288
    /**
289
     * @return object
290
     */
291
    public function newProto(): object
292
    {
293
        return $this->class->newInstanceWithoutConstructor();
294
    }
295
296
    /**
297
     * Sets a value through reflection.
298
     *
299
     * @param object $object
300
     * @param string $property
301
     * @param mixed $value
302
     */
303
    public function setValue(object $object, string $property, $value): void
304
    {
305
        $this->properties[$property]->setValue($object, $value);
306
    }
307
}
308