Completed
Pull Request — master (#380)
by Marc
02:38
created

BooleanParser::convertNodeToBoolean()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 4
nc 2
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace TYPO3Fluid\Fluid\Core\Parser;
3
4
/*
5
 * This file belongs to the package "TYPO3 Fluid".
6
 * See LICENSE.txt that was shipped with this package.
7
 */
8
9
/**
10
 * This BooleanParser helps to parse and evaluate boolean expressions.
11
 * it's basically a recursive decent parser that uses a tokenizing regex
12
 * to walk a given expression while evaluating each step along the way.
13
 *
14
 * For a basic recursive decent exampel check out:
15
 * http://stackoverflow.com/questions/2093138/what-is-the-algorithm-for-parsing-expressions-in-infix-notation
16
 *
17
 * Parsingtree:
18
 *
19
 *  evaluate/compile: start the whole cycle
20
 *      parseOrToken: takes care of "||" parts
21
 *          evaluateOr: evaluate the "||" part if found
22
 *          parseAndToken: take care of "&&" parts
23
 *              evaluateAnd: evaluate "&&" part if found
24
 *              parseCompareToken: takes care any comparisons "==,!=,>,<,..."
25
 *                  evaluateCompare: evaluate the comparison if found
26
 *                  parseNotToken: takes care of any "!" negations
27
 *                      evaluateNot: evaluate the negation if found
28
 *                      parseBracketToken: takes care of any '()' parts and restarts the cycle
29
 *                          parseStringToken: takes care of any strings
30
 *                              evaluateTerm: evaluate terms from true/false/numeric/context
31
 *
32
 */
33
class BooleanParser
34
{
35
36
    /**
37
     * List of comparators to check in the parseCompareToken if the current
38
     * part of the expression is a comparator and needs to be compared
39
     */
40
    const COMPARATORS = '==,===,!==,!=,<=,>=,<,>,%';
41
42
    /**
43
     * Regex to parse a expression into tokens
44
     */
45
    const TOKENREGEX = '/
46
			\s*(
47
				\\\\\'
48
			|
49
				\\"
50
			|
51
				[\'"]
52
			|
53
				[A-Za-z0-9\.\{\}\-\\\\]+
54
			|
55
				\=\=\=
56
			|
57
				\=\=
58
			|
59
				!\=\=
60
			|
61
				!\=
62
			|
63
				<\=
64
			|
65
				>\=
66
			|
67
				<
68
			|
69
				>
70
			|
71
				%
72
			|
73
				\|\|
74
			|
75
			    [aA][nN][dD]
76
			|
77
				&&
78
			|
79
			    [oO][rR]
80
			|
81
				.?
82
			)\s*
83
	/xsu';
84
85
    /**
86
     * Cursor that contains a integer value pointing to the location inside the
87
     * expression string that is used by the peek function to look for the part of
88
     * the expression that needs to be focused on next. This cursor is changed
89
     * by the consume method, by "consuming" part of the expression.
90
     *
91
     * @var integer
92
     */
93
    protected $cursor = 0;
94
95
    /**
96
     * Expression that is parsed through peek and consume methods
97
     *
98
     * @var string
99
     */
100
    protected $expression;
101
102
    /**
103
     * Context containing all variables that are references in the expression
104
     *
105
     * @var array
106
     */
107
    protected $context;
108
109
    /**
110
     * Switch to enable compiling
111
     *
112
     * @var boolean
113
     */
114
    protected $compileToCode = false;
115
116
    /**
117
     * Evaluate a expression to a boolean
118
     *
119
     * @param string $expression to be parsed
120
     * @param array $context containing variables that can be used in the expression
121
     * @return boolean
122
     */
123
    public function evaluate($expression, $context)
124
    {
125
        $this->context = $context;
126
        $this->expression = $expression;
127
        $this->cursor = 0;
128
        return $this->parseOrToken();
129
    }
130
131
    /**
132
     * Parse and compile an expression into an php equivalent
133
     *
134
     * @param string $expression to be parsed
135
     * @return string
136
     */
137
    public function compile($expression)
138
    {
139
        $this->expression = $expression;
140
        $this->cursor = 0;
141
        $this->compileToCode = true;
142
        return $this->parseOrToken();
143
    }
144
145
    /**
146
     * The part of the expression we're currently focusing on based on the
147
     * tokenizing regex offset by the internally tracked cursor.
148
     *
149
     * @param boolean $includeWhitespace return surrounding whitespace with token
150
     * @return string
151
     */
152
    protected function peek($includeWhitespace = false)
153
    {
154
        preg_match(static::TOKENREGEX, mb_substr($this->expression, $this->cursor), $matches);
155
        if ($includeWhitespace === true) {
156
            return $matches[0];
157
        }
158
        return $matches[1];
159
    }
160
161
    /**
162
     * Consume part of the current expression by setting the internal cursor
163
     * to the position of the string in the expression and it's length
164
     *
165
     * @param string $string
166
     * @return void
167
     */
168
    protected function consume($string)
169
    {
170
        if (mb_strlen($string) === 0) {
171
            return;
172
        }
173
        $this->cursor = mb_strpos($this->expression, $string, $this->cursor) + mb_strlen($string);
174
    }
175
176
    /**
177
     * Passes the torch down to the next deeper parsing leve (and)
178
     * and checks then if there's a "or" expression that needs to be handled
179
     *
180
     * @return mixed
181
     */
182 View Code Duplication
    protected function parseOrToken()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
183
    {
184
        $x = $this->parseAndToken();
185
        while (($token = $this->peek()) && in_array(strtolower($token), ['||', 'or'])) {
186
            $this->consume($token);
187
            $y = $this->parseAndToken();
188
189
            if ($this->compileToCode === true) {
190
                $x = '(' . $x . ' || ' . $y . ')';
191
                continue;
192
            }
193
            $x = $this->evaluateOr($x, $y);
194
        }
195
        return $x;
196
    }
197
198
    /**
199
     * Passes the torch down to the next deeper parsing leve (compare)
200
     * and checks then if there's a "and" expression that needs to be handled
201
     *
202
     * @return mixed
203
     */
204 View Code Duplication
    protected function parseAndToken()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
205
    {
206
        $x = $this->parseCompareToken();
207
        while (($token = $this->peek()) && in_array(strtolower($token), ['&&', 'and'])) {
208
            $this->consume($token);
209
            $y = $this->parseCompareToken();
210
211
            if ($this->compileToCode === true) {
212
                $x = '(' . $x . ' && ' . $y . ')';
213
                continue;
214
            }
215
            $x = $this->evaluateAnd($x, $y);
216
        }
217
        return $x;
218
    }
219
220
    /**
221
     * Passes the torch down to the next deeper parsing leven (not)
222
     * and checks then if there's a "compare" expression that needs to be handled
223
     *
224
     * @return mixed
225
     */
226
    protected function parseCompareToken()
227
    {
228
        $x = $this->parseNotToken();
229
        while (in_array($comparator = $this->peek(), explode(',', static::COMPARATORS))) {
230
            $this->consume($comparator);
231
            $y = $this->parseNotToken();
232
            $x = $this->evaluateCompare($x, $y, $comparator);
233
        }
234
        return $x;
235
    }
236
237
    /**
238
     * Check if we have encountered an not expression or pass the torch down
239
     * to the simpleToken method.
240
     *
241
     * @return mixed
242
     */
243
    protected function parseNotToken()
244
    {
245
        if ($this->peek() === '!') {
246
            $this->consume('!');
247
            $x = $this->parseNotToken();
248
249
            if ($this->compileToCode === true) {
250
                return '!(' . $x . ')';
251
            }
252
            return $this->evaluateNot($x);
253
        }
254
255
        return $this->parseBracketToken();
256
    }
257
258
    /**
259
     * Takes care of restarting the whole parsing loop if it encounters a "(" or ")"
260
     * token or pass the torch down to the parseStringToken method
261
     *
262
     * @return mixed
263
     */
264
    protected function parseBracketToken()
265
    {
266
        $t = $this->peek();
267
        if ($t === '(') {
268
            $this->consume('(');
269
            $result = $this->parseOrToken();
270
            $this->consume(')');
271
            return $result;
272
        }
273
274
        return $this->parseStringToken();
275
    }
276
277
    /**
278
     * Takes care of consuming pure string including whitespace or passes the torch
279
     * down to the parseTermToken method
280
     *
281
     * @return mixed
282
     */
283
    protected function parseStringToken()
284
    {
285
        $t = $this->peek();
286
        if ($t === '\'' || $t === '"') {
287
            $stringIdentifier = $t;
288
            $string = $stringIdentifier;
289
            $this->consume($stringIdentifier);
290
            while (trim($t = $this->peek(true)) !== $stringIdentifier) {
291
                $this->consume($t);
292
                $string .= $t;
293
            }
294
            $this->consume($stringIdentifier);
295
            $string .= $stringIdentifier;
296
            if ($this->compileToCode === true) {
297
                return $string;
298
            }
299
            return $this->evaluateTerm($string, $this->context);
300
        }
301
302
        return $this->parseTermToken();
303
    }
304
305
    /**
306
     * Takes care of restarting the whole parsing loop if it encounters a "(" or ")"
307
     * token, consumes a pure string including whitespace or passes the torch
308
     * down to the evaluateTerm method
309
     *
310
     * @return mixed
311
     */
312
    protected function parseTermToken()
313
    {
314
        $t = $this->peek();
315
        $this->consume($t);
316
        return $this->evaluateTerm($t, $this->context);
317
    }
318
319
    /**
320
     * Evaluate an "and" comparison
321
     *
322
     * @param mixed $x
323
     * @param mixed $y
324
     * @return boolean
325
     */
326
    protected function evaluateAnd($x, $y)
327
    {
328
        return $x && $y;
329
    }
330
331
    /**
332
     * Evaluate an "or" comparison
333
     *
334
     * @param mixed $x
335
     * @param mixed $y
336
     * @return boolean
337
     */
338
    protected function evaluateOr($x, $y)
339
    {
340
        return $x || $y;
341
    }
342
343
    /**
344
     * Evaluate an "not" comparison
345
     *
346
     * @param mixed $x
347
     * @return boolean|string
348
     */
349
    protected function evaluateNot($x)
350
    {
351
        return !$x;
352
    }
353
354
    /**
355
     * Compare two variables based on a specified comparator
356
     *
357
     * @param mixed $x
358
     * @param mixed $y
359
     * @param string $comparator
360
     * @return boolean|string
361
     */
362
    protected function evaluateCompare($x, $y, $comparator)
363
    {
364
        // enfore strong comparison for comparing two objects
365
        if ($comparator == '==' && is_object($x) && is_object($y)) {
366
            $comparator = '===';
367
        }
368
        if ($comparator == '!=' && is_object($x) && is_object($y)) {
369
            $comparator = '!==';
370
        }
371
372
        if ($this->compileToCode === true) {
373
            return sprintf('(%s %s %s)', $x, $comparator, $y);
374
        }
375
376
        switch ($comparator) {
377
            case '==':
378
                $x = ($x == $y);
379
                break;
380
381
            case '===':
382
                $x = ($x === $y);
383
                break;
384
385
            case '!=':
386
                $x = ($x != $y);
387
                break;
388
389
            case '!==':
390
                $x = ($x !== $y);
391
                break;
392
393
            case '<=':
394
                $x = ($x <= $y);
395
                break;
396
397
            case '>=':
398
                $x = ($x >= $y);
399
                break;
400
401
            case '<':
402
                $x = ($x < $y);
403
                break;
404
405
            case '>':
406
                $x = ($x > $y);
407
                break;
408
409
            case '%':
410
                $x = ($x % $y);
411
                break;
412
        }
413
        return $x;
414
    }
415
416
    /**
417
     * Takes care of fetching terms from the context, converting to float/int,
418
     * converting true/false keywords into boolean or trim the final string of
419
     * quotation marks
420
     *
421
     * @param string $x
422
     * @param array $context
423
     * @return mixed
424
     */
425
    protected function evaluateTerm($x, $context)
426
    {
427
        if (isset($context[$x]) || (mb_strpos($x, '{') === 0 && mb_substr($x, -1) === '}')) {
428
            if ($this->compileToCode === true) {
429
                return BooleanParser::class . '::convertNodeToBoolean($context["' . trim($x, '{}') . '"])';
430
            }
431
            return self::convertNodeToBoolean($context[trim($x, '{}')]);
432
        }
433
434
        if (is_numeric($x)) {
435
            if ($this->compileToCode === true) {
436
                return $x;
437
            }
438
            if (mb_strpos($x, '.') !== false) {
439
                return (float)$x;
440
            } else {
441
                return (int)$x;
442
            }
443
        }
444
445 View Code Duplication
        if (trim(strtolower($x)) === 'true') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
446
            if ($this->compileToCode === true) {
447
                return 'TRUE';
448
            }
449
            return true;
450
        }
451 View Code Duplication
        if (trim(strtolower($x)) === 'false') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
452
            if ($this->compileToCode === true) {
453
                return 'FALSE';
454
            }
455
            return false;
456
        }
457
458
        if ($this->compileToCode === true) {
459
            return '"' . trim($x, '\'"') . '"';
460
        }
461
462
        return trim($x, '\'"');
463
    }
464
465
    public static function convertNodeToBoolean($value) {
466
        if (is_object($value) && $value instanceof \Countable) {
467
            return count($value) > 0;
468
        }
469
        return $value;
470
    }
471
}
472