Reflection::getUniqueGroups()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 5
nc 3
nop 0
dl 0
loc 9
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
        // reflection
165
        if ($type = $this->properties[$property]->getType() and $type instanceof ReflectionNamedType) {
166
            return static::SCALARS[$type->getName()] ?? $type->getName();
167
        }
168
169
        // @var
170
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
171
            $type = preg_replace(static::RX_NULL, '', $var['type']); // remove null
172
            // it's beyond the scope of this class to parse "use" statements (for now),
173
            // @var <CLASS> must be a FQN in order to work.
174
            return static::SCALARS[$type] ?? ltrim($type, '\\');
175
        }
176
177
        // default value
178
        return static::SCALARS[gettype($this->defaults[$property])];
179
    }
180
181
    /**
182
     * Columns with their own individual unique constraints.
183
     *
184
     * @return string[]
185
     */
186
    public function getUnique(): array
187
    {
188
        return array_filter($this->columns, fn(string $property) => $this->isUnique($property));
189
    }
190
191
    /**
192
     * Table-level (multi-column) constraints are grouped under an arbitrary shared identifier.
193
     *
194
     * `[ shared identifier => [ col, ... ], ... ]`
195
     *
196
     * @return string[][]
197
     */
198
    public function getUniqueGroups(): array
199
    {
200
        $groups = [];
201
        foreach ($this->columns as $column) {
202
            if ($this->isUniqueMulti($column, $ident)) {
203
                $groups[$ident][$column] = $column;
204
            }
205
        }
206
        return $groups;
207
    }
208
209
    /**
210
     * Gets and returns a value through reflection.
211
     *
212
     * @param object $object
213
     * @param string $property
214
     * @return mixed
215
     */
216
    public function getValue(object $object, string $property)
217
    {
218
        return $this->properties[$property]->getValue($object);
219
    }
220
221
    /**
222
     * @param string $property
223
     * @return bool
224
     */
225
    public function isColumn(string $property): bool
226
    {
227
        return (bool)preg_match(static::RX_IS_COLUMN, $this->properties[$property]->getDocComment());
228
    }
229
230
    /**
231
     * Whether a property allows `NULL` as a value.
232
     *
233
     * @param string $property
234
     * @return bool
235
     */
236
    public function isNullable(string $property): bool
237
    {
238
        if ($type = $this->properties[$property]->getType()) {
239
            return $type->allowsNull();
240
        }
241
        if (preg_match(static::RX_VAR, $this->properties[$property]->getDocComment(), $var)) {
242
            return (bool)preg_match(static::RX_NULL, $var['type']);
243
        }
244
        return $this->defaults[$property] === null;
245
    }
246
247
    /**
248
     * @param string $column
249
     * @return bool
250
     */
251
    public function isUnique(string $column): bool
252
    {
253
        return (bool)preg_match(static::RX_UNIQUE, $this->properties[$column]->getDocComment());
254
    }
255
256
    /**
257
     * @param string $column
258
     * @param void $ident Updated to the group identifier.
259
     * @return bool
260
     */
261
    public function isUniqueMulti(string $column, &$ident = null): bool
262
    {
263
        if (preg_match(static::RX_UNIQUE_GROUP, $this->properties[$column]->getDocComment(), $unique)) {
264
            $ident = $unique['ident'];
265
            return true;
266
        }
267
        return false;
268
    }
269
270
    /**
271
     * @return object
272
     */
273
    public function newProto(): object
274
    {
275
        return $this->class->newInstanceWithoutConstructor();
276
    }
277
278
    /**
279
     * Sets a value through reflection.
280
     *
281
     * @param object $object
282
     * @param string $property
283
     * @param mixed $value
284
     */
285
    public function setValue(object $object, string $property, $value): void
286
    {
287
        $this->properties[$property]->setValue($object, $value);
288
    }
289
}
290