Passed
Push — master ( b3ece8...6db175 )
by y
01:34
created

Reflection::getValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
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.
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