Passed
Push — master ( 285c17...9cf790 )
by y
02:17 queued 13s
created

Reflection::isUnique()   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
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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*@unique(\h+(?<ident>[a-z_]+))?/i';
27
    protected const RX_VAR = '/\*\h*@var\h+(?<type>\S+)/i'; // includes pipes and backslashes
28
    protected const RX_NULL = '/(\bnull\|)|(\|null\b)/i';
29
    protected const RX_EAV = '/\*\h*@eav\h+(?<table>\w+)/i';
30
    protected const RX_EAV_VAR = '/\*\h*@var\h+(?<type>\w+)\[\]/i'; // typed array
31
    protected const RX_JUNCTION = '/\*\h*@junction\h+(?<table>\w+)/i';
32
    protected const RX_FOREIGN = '/\*\h*@foreign\h+(?<column>\w+)\h+(?<class>\S+)/i';
33
34
    /**
35
     * Maps annotated/native scalar types to storage types acceptable for `settype()`
36
     *
37
     * @see Schema::T_CONST_NAMES keys
38
     */
39
    const SCALARS = [
40
        'bool' => 'bool',
41
        'boolean' => 'bool',    // gettype()
42
        'double' => 'float',    // gettype()
43
        'false' => 'bool',      // @var
44
        'float' => 'float',
45
        'int' => 'int',
46
        'integer' => 'int',     // gettype()
47
        'NULL' => 'string',     // gettype()
48
        'number' => 'string',   // @var
49
        'scalar' => 'string',   // @var
50
        'string' => 'string',
51
        'String' => 'String',   // @var
52
        'STRING' => 'STRING',   // @var
53
        'true' => 'bool',       // @var
54
    ];
55
56
    /**
57
     * @var ReflectionClass
58
     */
59
    protected $class;
60
61
    /**
62
     * @var string[]
63
     */
64
    protected $columns;
65
66
    /**
67
     * @var DB
68
     */
69
    protected $db;
70
71
    /**
72
     * @var array
73
     */
74
    protected $defaults = [];
75
76
    /**
77
     * @var ReflectionProperty[]
78
     */
79
    protected $properties = [];
80
81
    /**
82
     * @var string[]
83
     */
84
    protected $unique;
85
86
    /**
87
     * @param DB $db
88
     * @param string|object $class
89
     */
90
    public function __construct(DB $db, $class)
91
    {
92
        $this->db = $db;
93
        $this->class = new ReflectionClass($class);
94
        $this->defaults = $this->class->getDefaultProperties();
95
        foreach ($this->class->getProperties() as $property) {
96
            $property->setAccessible(true);
97
            $this->properties[$property->getName()] = $property;
98
        }
99
    }
100
101
    /**
102
     * @TODO allow aliasing
103
     *
104
     * @return string[]
105
     */
106
    public function getColumns(): array
107
    {
108
        if (!isset($this->columns)) {
109
            $this->columns = [];
110
            foreach ($this->properties as $name => $property) {
111
                if (preg_match(static::RX_IS_COLUMN, $property->getDocComment())) {
112
                    $this->columns[$name] = $name;
113
                }
114
            }
115
        }
116
        return $this->columns;
117
    }
118
119
    /**
120
     * @return EAV[]
121
     */
122
    public function getEav()
123
    {
124
        $EAV = [];
125
        foreach ($this->properties as $name => $prop) {
126
            $doc = $prop->getDocComment();
127
            if (preg_match(static::RX_EAV, $doc, $eav)) {
128
                preg_match(static::RX_EAV_VAR, $doc, $var);
129
                $type = $var['type'] ?? 'string';
130
                $type = static::SCALARS[$type] ?? 'string';
131
                $EAV[$name] = EAV::factory($this->db, $eav['table'], $type);
132
            }
133
        }
134
        return $EAV;
135
    }
136
137
    /**
138
     * Classes keyed by column.
139
     *
140
     * @return string[]
141
     */
142
    public function getForeignClasses(): array
143
    {
144
        preg_match_all(static::RX_FOREIGN, $this->class->getDocComment(), $foreign, PREG_SET_ORDER);
145
        return array_column($foreign, 'class', 'column');
146
    }
147
148
    /**
149
     * @return string
150
     */
151
    public function getJunctionTable(): string
152
    {
153
        preg_match(static::RX_JUNCTION, $this->class->getDocComment(), $junction);
154
        return $junction['table'];
155
    }
156
157
    /**
158
     * @return string
159
     */
160
    public function getRecordTable(): string
161
    {
162
        preg_match(static::RX_RECORD, $this->class->getDocComment(), $record);
163
        return $record['table'];
164
    }
165
166
    /**
167
     * Scalar storage type, or FQN of a complex type.
168
     *
169
     * @param string $property
170
     * @return string
171
     */
172
    public function getType(string $property): string
173
    {
174
        return $this->getType_reflection($property)
175
            ?? $this->getType_var($property)
176
            ?? static::SCALARS[gettype($this->defaults[$property])];
177
    }
178
179
    /**
180
     * @param string $property
181
     * @return null|string
182
     */
183
    protected function getType_reflection(string $property): ?string
184
    {
185
        if ($type = $this->properties[$property]->getType() and $type instanceof ReflectionNamedType) {
186
            return static::SCALARS[$type->getName()] ?? $type->getName();
187
        }
188
        return null;
189
    }
190
191
    /**
192
     * @param string $property
193
     * @return null|string
194
     */
195
    protected function getType_var(string $property): ?string
196
    {
197
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
198
            $type = preg_replace(static::RX_NULL, '', $var['type']); // remove null
199
            if (isset(static::SCALARS[$type])) {
200
                return static::SCALARS[$type];
201
            }
202
            // it's beyond the scope of this class to parse "use" statements (for now),
203
            // @var <CLASS> must be a FQN in order to work.
204
            return ltrim($type, '\\');
205
        }
206
        return null;
207
    }
208
209
    /**
210
     * Column groupings for unique constraints.
211
     * - Column-level constraints are enumerated names.
212
     * - Table-level (multi-column) constraints are names grouped under an arbitrary shared identifier.
213
     *
214
     * `[ 'foo', 'my_multi'=>['bar','baz'], ... ]`
215
     *
216
     * @return array
217
     */
218
    public function getUnique()
219
    {
220
        if (!isset($this->unique)) {
221
            $this->unique = [];
222
            foreach ($this->properties as $property) {
223
                if (preg_match(static::RX_UNIQUE, $property->getDocComment(), $match)) {
224
                    if (isset($match['ident'])) {
225
                        $this->unique[$match['ident']][] = $property->getName();
226
                    } else {
227
                        $this->unique[] = $property->getName();
228
                    }
229
                }
230
            }
231
        }
232
        return $this->unique;
233
    }
234
235
    /**
236
     * The shared identifier if a property is part of a multi-column unique-key.
237
     *
238
     * @param string $property
239
     * @return null|string The shared identifier, or nothing.
240
     */
241
    final public function getUniqueGroup(string $property): ?string
242
    {
243
        foreach ($this->getUnique() as $key => $value) {
244
            if (is_string($key) and in_array($property, $value)) {
0 ignored issues
show
Bug introduced by
$value of type string is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

244
            if (is_string($key) and in_array($property, /** @scrutinizer ignore-type */ $value)) {
Loading history...
245
                return $key;
246
            }
247
        }
248
        return null;
249
    }
250
251
    /**
252
     * Gets and returns a value through reflection.
253
     *
254
     * @param object $object
255
     * @param string $property
256
     * @return mixed
257
     */
258
    public function getValue(object $object, string $property)
259
    {
260
        return $this->properties[$property]->getValue($object);
261
    }
262
263
    /**
264
     * Whether a property allows `NULL` as a value.
265
     *
266
     * @param string $property
267
     * @return bool
268
     */
269
    public function isNullable(string $property): bool
270
    {
271
        if ($type = $this->properties[$property]->getType()) {
272
            return $type->allowsNull();
273
        }
274
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
275
            return (bool)preg_match(static::RX_NULL, $var['type']);
276
        }
277
        return $this->defaults[$property] === null;
278
    }
279
280
    /**
281
     * Whether a property has a unique-key constraint of its own.
282
     *
283
     * @param string $property
284
     * @return bool
285
     */
286
    final public function isUnique(string $property): bool
287
    {
288
        return in_array($property, $this->getUnique());
289
    }
290
291
    /**
292
     * @return object
293
     */
294
    public function newProto(): object
295
    {
296
        return $this->class->newInstanceWithoutConstructor();
297
    }
298
299
    /**
300
     * Sets a value through reflection.
301
     *
302
     * @param object $object
303
     * @param string $property
304
     * @param mixed $value
305
     */
306
    public function setValue(object $object, string $property, $value): void
307
    {
308
        $this->properties[$property]->setValue($object, $value);
309
    }
310
}
311