Passed
Pull Request — 2.0 (#56)
by Vincent
16:03 queued 09:35
created

ExpressionCompiler   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 206
Duplicated Lines 0 %

Test Coverage

Coverage 96.49%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 20
eloc 71
c 1
b 0
f 0
dl 0
loc 206
ccs 55
cts 57
cp 0.9649
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
A compileName() 0 15 3
A instance() 0 7 2
A compile() 0 23 5
A compileAlias() 0 11 2
A compileStatic() 0 16 2
A compileAttribute() 0 8 1
A compileDynamic() 0 24 5
1
<?php
2
3
namespace Bdf\Prime\Query\Compiler\AliasResolver;
4
5
use Bdf\Prime\Exception\QueryBuildingException;
6
7
/**
8
 * Compiler for relations and attributes expressions
9
 *
10
 * Syntax :
11
 * =========
12
 * $NAME  => ($CHR | _ | #)+
13
 * $ATTR  => >$NAME
14
 * $ALIAS => $$NAME
15
 * $DYN   => $NAME(.$NAME)*
16
 * $STA   => "$DYN"
17
 * $EXPR =>
18
 *    ($ALIAS | $STA)($DYN | $ATTR)
19
 *    $DYN($ATTR)?
20
 *    $ATTR
21
 *
22
 * Definitions :
23
 * ==============
24
 * $NAME :
25
 *      The part name.
26
 *      Can be relation or attribute.
27
 *
28
 * $ATTR :
29
 *      Force specify that is an attribute.
30
 *      Should be the last part of the expression.
31
 *      Should not be analyzed.
32
 *
33
 * $ALIAS :
34
 *      A registered alias.
35
 *      Should be the first part of the expression.
36
 *      The alias represents a (possibly) deep relation path.
37
 *      The alias should not be analyzed, only check the metadata table.
38
 *      Cannot represents an attribute, so an alias should be followed by one of $DYN or $ATTR.
39
 *
40
 * $DYN :
41
 *      The dynamic expression.
42
 *      Always analyzed, can be attribute, or relation path.
43
 *      This expression is sufficient for most of request.
44
 *      This is the only expression that can be preceded AND flowed by tokens
45
 *
46
 * $STA :
47
 *      The "static" expression.
48
 *      A static expression is ALWAYS related to an alias.
49
 *      So, one $STA is related to one AND ONLY one $ALIAS.
50
 *      Also has save constrains than $ALIAS (should be the first token, cannot respresents an attribute...).
51
 *
52
 * $EXPR :
53
 *      The complete expression.
54
 *      Should represents the complete relations path plus the attribute.
55
 *      This is a database value.
56
 *
57
 * Example :
58
 * ==========
59
 *
60
 * All those examples do the same
61
 *
62
 * user.location.address.name :
63
 *      $DYN = user location address name
64
 *      Will :
65
 *          - resolve user, and set as t1
66
 *          - resolve user.location and set as t2
67
 *          - Find that address.name is an attribute of user.location
68
 *      Conclusion :
69
 *          The simple way to do that. But resolve too many times.
70
 *          So for multiple queries on same attribute path, do useless resolutions
71
 *
72
 * "user.location"address.name :
73
 *      $STA = user.location
74
 *      $ATTR = address name
75
 *      Will :
76
 *          - Check in the path table user.location
77
 *              - If exists, get the metadata
78
 *              - If not, resolve as $DYN, and store the path
79
 *          - Find that address.name is an attribute of user.location
80
 *      Conclusion :
81
 *          If many filters applies on user.location, the metadata and path are already loaded
82
 *          So do not add overhead for useless resolutions
83
 *          But use the $DYN algo for find the attribute, that can decreases performances
84
 *
85
 * "user.location">address.name :
86
 *      $STA = user.location
87
 *      $ATTR = address.name
88
 *      Will :
89
 *          - Check user.location in the path table
90
 *          - use address.name as attribute
91
 *      Conclusion :
92
 *          The best usage. Remove overhead for resolve relations path AND for resolving attribute name
93
 *
94
 * $t2>address.name
95
 *      $ALIAS = t2
96
 *      $ATTR = address.name
97
 *      Will :
98
 *          - Get t2 alias
99
 *          - use address.name as attribute
100
 *      Conclusion :
101
 *          Same as "user.location">address.name, but the user should care about the alias.
102
 *          The best for internal usages
103
 *
104
 * $t1.location>address.name :
105
 *      $ALIAS = t1
106
 *      $DYN = location
107
 *      $ATTR = address.name
108
 *      Will :
109
 *          - Get the t1 alias
110
 *          - Resolve location into t1 (via $DYN)
111
 *          - use address.name as attribute
112
 *      Conclusion ;
113
 *          The worst way, but can be usefull internally if (and only if)
114
 *              - The context is an alias
115
 *              - Cannot ensure that the "right part" is an attribute
116
 *              - The real attribute is known
117
 */
118
class ExpressionCompiler
119
{
120
    public const DYN_SEPARATOR    = '.';
121
    public const ATTR_IDENTIFIER  = '>';
122
    public const STA_IDENTIFIER   = '"';
123
    public const ALIAS_IDENTIFIER = '$';
124
125
    public const RESERVED = [
126
        self::DYN_SEPARATOR    => true,
127
        self::ATTR_IDENTIFIER  => true,
128
        self::STA_IDENTIFIER   => true,
129
        self::ALIAS_IDENTIFIER => true,
130
    ];
131
132
    /**
133
     * @var static
134
     */
135
    private static $instance;
136
137
    /**
138
     * Compile the expression to expression tokens
139
     *
140
     * @param string $expression
141
     *
142
     * @return ExpressionToken[]
143
     */
144 145
    public function compile($expression)
145
    {
146 145
        $len = strlen($expression);
147 145
        $pos = 0;
148 145
        $tokens = [];
149
150 145
        while ($pos < $len) {
151 145
            switch ($expression[$pos]) {
152 145
                case self::ALIAS_IDENTIFIER:
153 71
                    $tokens[] = $this->compileAlias($expression, $pos, $len);
154 71
                    break;
155 145
                case self::STA_IDENTIFIER:
156 5
                    $tokens[] = $this->compileStatic($expression, $pos, $len);
157 5
                    break;
158 145
                case self::ATTR_IDENTIFIER:
159 106
                    $tokens[] = $this->compileAttribute($expression, $pos, $len);
160 106
                    break;
161
                default:
162 141
                    $tokens[] = $this->compileDynamic($expression, $pos, $len);
163
            }
164
        }
165
166 145
        return $tokens;
167
    }
168
169
    /**
170
     * Compile $ALIAS.
171
     *
172
     * $t1 => new ExpressionToken(TYPE_ALIAS, 't1')
173
     *
174
     * @param string $expression
175
     * @param int $pos
176
     * @param int $len
177
     *
178
     * @return ExpressionToken
179
     */
180 71
    protected function compileAlias($expression, &$pos, $len)
181
    {
182 71
        if ($pos !== 0) {
183
            throw new QueryBuildingException('Alias should be the first expression token');
184
        }
185
186 71
        ++$pos;
187
188 71
        return new ExpressionToken(
189
            ExpressionToken::TYPE_ALIAS,
190 71
            $this->compileName($expression, $pos, $len)
191
        );
192
    }
193
194
    /**
195
     * Compile $ATTR
196
     *
197
     * xxx>my.attribute => new ExpressionToken(TYPE_ATTR, 'my.attribute')
198
     *
199
     * @param string $expression
200
     * @param int $pos
201
     * @param int $len
202
     *
203
     * @return ExpressionToken
204
     */
205 106
    protected function compileAttribute($expression, &$pos, $len)
206
    {
207 106
        $value = substr($expression, $pos + 1);
208 106
        $pos = $len;
209
210 106
        return new ExpressionToken(
211
            ExpressionToken::TYPE_ATTR,
212
            $value
213
        );
214
    }
215
216
    /**
217
     * Compile a $STA
218
     *
219
     * "my.static.exp" => new ExpressionToken(TYPE_STA, 'my.static.exp')
220
     *
221
     * @param string $expression
222
     * @param int $pos
223
     * @param int $len
224
     *
225
     * @return ExpressionToken
226
     */
227 5
    protected function compileStatic($expression, &$pos, $len)
0 ignored issues
show
Unused Code introduced by
The parameter $len is not used and could be removed. ( Ignorable by Annotation )

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

227
    protected function compileStatic($expression, &$pos, /** @scrutinizer ignore-unused */ $len)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
228
    {
229 5
        if ($pos !== 0) {
230
            throw new QueryBuildingException('Static expression should be the first expression token');
231
        }
232
233 5
        ++$pos;
234 5
        $end = strpos($expression, self::STA_IDENTIFIER, $pos);
235
236 5
        $value = substr($expression, $pos, $end - $pos);
237
238 5
        $pos = $end + 1;
239
240 5
        return new ExpressionToken(
241
            ExpressionToken::TYPE_STA,
242
            $value
243
        );
244
    }
245
246
    /**
247
     * Compile an $DYN
248
     *
249
     * my.super.expr => new ExpressionToken(TYPE_DYN, ['my', 'super', 'expr'])
250
     *
251
     * @param string $expression
252
     * @param int $pos
253
     * @param int $len
254
     *
255
     * @return ExpressionToken
256
     */
257 141
    protected function compileDynamic($expression, &$pos, $len)
258
    {
259 141
        if ($expression[$pos] === self::DYN_SEPARATOR) {
260 52
            ++$pos;
261
        }
262
263 141
        $names = [];
264
265 141
        for (;;) {
266 141
            $names[] = $this->compileName($expression, $pos, $len);
267
268
            if (
269
                $pos >= $len
270 141
                || $expression[$pos] !== self::DYN_SEPARATOR
271
            ) {
272 141
                break;
273
            }
274
275 141
            ++$pos;
276
        }
277
278 141
        return new ExpressionToken(
279
            ExpressionToken::TYPE_DYN,
280
            $names
281
        );
282
    }
283
284
    /**
285
     * Compile a $NAME.
286
     *
287
     * The compilation stops when encounter a reserved character, or the end of the expression
288
     *
289
     * @param string $expression
290
     * @param int $pos
291
     * @param int $len
292
     *
293
     * @return string
294
     */
295 142
    protected function compileName($expression, &$pos, $len)
296
    {
297 142
        $name = '';
298
299 142
        for (;$pos < $len; ++$pos) {
300 142
            $chr = $expression[$pos];
301
302 142
            if (array_key_exists($chr, self::RESERVED)) {
303 141
                break;
304
            }
305
306 142
            $name .= $chr;
307
        }
308
309 142
        return $name;
310
    }
311
312
    /**
313
     * Get the compiler instance
314
     *
315
     * @return static
316
     */
317 136
    public static function instance()
318
    {
319 136
        if (static::$instance === null) {
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $instance to at least protected.
Loading history...
320 1
            static::$instance = new static();
321
        }
322
323 136
        return static::$instance;
324
    }
325
}
326