Completed
Pull Request — master (#291)
by Claus
02:44
created

BooleanParser   C

Complexity

Total Complexity 60

Size/Duplication

Total Lines 428
Duplicated Lines 9.81 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 42
loc 428
rs 6.0975
wmc 60
lcom 1
cbo 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A evaluate() 0 7 1
A compile() 0 7 1
A peek() 0 8 2
A consume() 0 7 2
A parseOrToken() 15 15 3
A parseAndToken() 15 15 3
A parseCompareToken() 0 10 2
A parseNotToken() 0 14 3
A parseBracketToken() 0 12 2
B parseStringToken() 0 21 5
A parseTermToken() 0 6 1
A evaluateAnd() 0 4 2
A evaluateOr() 0 4 2
A evaluateNot() 0 4 1
C evaluateCompare() 0 53 17
C evaluateTerm() 12 39 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like BooleanParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BooleanParser, and based on these observations, apply Extract Interface, too.

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
use TYPO3Fluid\Fluid\Core\Parser\Exception as ParserException;
10
11
/**
12
 * This BooleanParser helps to parse and evaluate boolean expressions.
13
 * it's basically a recursive decent parser that uses a tokenizing regex
14
 * to walk a given expression while evaluating each step along the way.
15
 *
16
 * For a basic recursive decent exampel check out:
17
 * http://stackoverflow.com/questions/2093138/what-is-the-algorithm-for-parsing-expressions-in-infix-notation
18
 *
19
 * Parsingtree:
20
 *
21
 *  evaluate/compile: start the whole cycle
22
 *      parseOrToken: takes care of "||" parts
23
 *          evaluateOr: evaluate the "||" part if found
24
 *          parseAndToken: take care of "&&" parts
25
 *              evaluateAnd: evaluate "&&" part if found
26
 *              parseCompareToken: takes care any comparisons "==,!=,>,<,..."
27
 *                  evaluateCompare: evaluate the comparison if found
28
 *                  parseNotToken: takes care of any "!" negations
29
 *                      evaluateNot: evaluate the negation if found
30
 *                      parseBracketToken: takes care of any '()' parts and restarts the cycle
31
 *                          parseStringToken: takes care of any strings
32
 *                              evaluateTerm: evaluate terms from true/false/numeric/context
33
 *
34
 */
35
class BooleanParser
36
{
37
38
    /**
39
     * List of comparators to check in the parseCompareToken if the current
40
     * part of the expression is a comparator and needs to be compared
41
     */
42
    const COMPARATORS = '==,===,!==,!=,<=,>=,<,>,%';
43
44
    /**
45
     * Regex to parse a expression into tokens
46
     */
47
    const TOKENREGEX = '/
48
			\s*(
49
				\\\\\'
50
			|
51
				\\"
52
			|
53
				[\'"]
54
			|
55
				[A-Za-z0-9\.\{\}\-\\\\]+
56
			|
57
				\=\=\=
58
			|
59
				\=\=
60
			|
61
				!\=\=
62
			|
63
				!\=
64
			|
65
				<\=
66
			|
67
				>\=
68
			|
69
				<
70
			|
71
				>
72
			|
73
				%
74
			|
75
				\|\|
76
			|
77
				&&
78
			|
79
				.?
80
			)\s*
81
	/xs';
82
83
    /**
84
     * Cursor that contains a integer value pointing to the location inside the
85
     * expression string that is used by the peek function to look for the part of
86
     * the expression that needs to be focused on next. This cursor is changed
87
     * by the consume method, by "consuming" part of the expression.
88
     *
89
     * @var integer
90
     */
91
    protected $cursor = 0;
92
93
    /**
94
     * Expression that is parsed through peek and consume methods
95
     *
96
     * @var string
97
     */
98
    protected $expression;
99
100
    /**
101
     * Context containing all variables that are references in the expression
102
     *
103
     * @var array
104
     */
105
    protected $context;
106
107
    /**
108
     * Switch to enable compiling
109
     *
110
     * @var boolean
111
     */
112
    protected $compileToCode = false;
113
114
    /**
115
     * Evaluate a expression to a boolean
116
     *
117
     * @param string $expression to be parsed
118
     * @param array $context containing variables that can be used in the expression
119
     * @return boolean
120
     */
121
    public function evaluate($expression, $context)
122
    {
123
        $this->context = $context;
124
        $this->expression = $expression;
125
        $this->cursor = 0;
126
        return $this->parseOrToken();
127
    }
128
129
    /**
130
     * Parse and compile an expression into an php equivalent
131
     *
132
     * @param string $expression to be parsed
133
     * @return string
134
     */
135
    public function compile($expression)
136
    {
137
        $this->expression = $expression;
138
        $this->cursor = 0;
139
        $this->compileToCode = true;
140
        return $this->parseOrToken();
141
    }
142
143
    /**
144
     * The part of the expression we're currently focusing on based on the
145
     * tokenizing regex offset by the internally tracked cursor.
146
     *
147
     * @param boolean $includeWhitespace return surrounding whitespace with token
148
     * @return string
149
     */
150
    protected function peek($includeWhitespace = false)
151
    {
152
        preg_match(static::TOKENREGEX, substr($this->expression, $this->cursor), $matches);
153
        if ($includeWhitespace === true) {
154
            return $matches[0];
155
        }
156
        return $matches[1];
157
    }
158
159
    /**
160
     * Consume part of the current expression by setting the internal cursor
161
     * to the position of the string in the expression and it's length
162
     *
163
     * @param string $string
164
     * @return void
165
     */
166
    protected function consume($string)
167
    {
168
        if (strlen($string) === 0) {
169
            return;
170
        }
171
        $this->cursor = strpos($this->expression, $string, $this->cursor) + strlen($string);
172
    }
173
174
    /**
175
     * Passes the torch down to the next deeper parsing leve (and)
176
     * and checks then if there's a "or" expression that needs to be handled
177
     *
178
     * @return mixed
179
     */
180 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...
181
    {
182
        $x = $this->parseAndToken();
183
        while ($this->peek() === '||') {
184
            $this->consume('||');
185
            $y = $this->parseAndToken();
186
187
            if ($this->compileToCode === true) {
188
                $x = '(' . $x . ' || ' . $y . ')';
189
                continue;
190
            }
191
            $x = $this->evaluateOr($x, $y);
192
        }
193
        return $x;
194
    }
195
196
    /**
197
     * Passes the torch down to the next deeper parsing leve (compare)
198
     * and checks then if there's a "and" expression that needs to be handled
199
     *
200
     * @return mixed
201
     */
202 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...
203
    {
204
        $x = $this->parseCompareToken();
205
        while ($this->peek() === '&&') {
206
            $this->consume('&&');
207
            $y = $this->parseCompareToken();
208
209
            if ($this->compileToCode === true) {
210
                $x = '(' . $x . ' && ' . $y . ')';
211
                continue;
212
            }
213
            $x = $this->evaluateAnd($x, $y);
214
        }
215
        return $x;
216
    }
217
218
    /**
219
     * Passes the torch down to the next deeper parsing leven (not)
220
     * and checks then if there's a "compare" expression that needs to be handled
221
     *
222
     * @return mixed
223
     */
224
    protected function parseCompareToken()
225
    {
226
        $x = $this->parseNotToken();
227
        while (in_array($comparator = $this->peek(), explode(',', static::COMPARATORS))) {
228
            $this->consume($comparator);
229
            $y = $this->parseNotToken();
230
            $x = $this->evaluateCompare($x, $y, $comparator);
231
        }
232
        return $x;
233
    }
234
235
    /**
236
     * Check if we have encountered an not expression or pass the torch down
237
     * to the simpleToken method.
238
     *
239
     * @return mixed
240
     */
241
    protected function parseNotToken()
242
    {
243
        if ($this->peek() === '!') {
244
            $this->consume('!');
245
            $x = $this->parseNotToken();
246
247
            if ($this->compileToCode === true) {
248
                return '!(' . $x . ')';
249
            }
250
            return $this->evaluateNot($x);
251
        }
252
253
        return $this->parseBracketToken();
254
    }
255
256
    /**
257
     * Takes care of restarting the whole parsing loop if it encounters a "(" or ")"
258
     * token or pass the torch down to the parseStringToken method
259
     *
260
     * @return mixed
261
     */
262
    protected function parseBracketToken()
263
    {
264
        $t = $this->peek();
265
        if ($t === '(') {
266
            $this->consume('(');
267
            $result = $this->parseOrToken();
268
            $this->consume(')');
269
            return $result;
270
        }
271
272
        return $this->parseStringToken();
273
    }
274
275
    /**
276
     * Takes care of consuming pure string including whitespace or passes the torch
277
     * down to the parseTermToken method
278
     *
279
     * @return mixed
280
     */
281
    protected function parseStringToken()
282
    {
283
        $t = $this->peek();
284
        if ($t === '\'' || $t === '"') {
285
            $stringIdentifier = $t;
286
            $string = $stringIdentifier;
287
            $this->consume($stringIdentifier);
288
            while (trim($t = $this->peek(true)) !== $stringIdentifier) {
289
                $this->consume($t);
290
                $string .= $t;
291
            }
292
            $this->consume($stringIdentifier);
293
            $string .= $stringIdentifier;
294
            if ($this->compileToCode === true) {
295
                return $string;
296
            }
297
            return $this->evaluateTerm($string, $this->context);
298
        }
299
300
        return $this->parseTermToken();
301
    }
302
303
    /**
304
     * Takes care of restarting the whole parsing loop if it encounters a "(" or ")"
305
     * token, consumes a pure string including whitespace or passes the torch
306
     * down to the evaluateTerm method
307
     *
308
     * @return mixed
309
     */
310
    protected function parseTermToken()
311
    {
312
        $t = $this->peek();
313
        $this->consume($t);
314
        return $this->evaluateTerm($t, $this->context);
315
    }
316
317
    /**
318
     * Evaluate an "and" comparison
319
     *
320
     * @param mixed $x
321
     * @param mixed $y
322
     * @return boolean
323
     */
324
    protected function evaluateAnd($x, $y)
325
    {
326
        return $x && $y;
327
    }
328
329
    /**
330
     * Evaluate an "or" comparison
331
     *
332
     * @param mixed $x
333
     * @param mixed $y
334
     * @return boolean
335
     */
336
    protected function evaluateOr($x, $y)
337
    {
338
        return $x || $y;
339
    }
340
341
    /**
342
     * Evaluate an "not" comparison
343
     *
344
     * @param mixed $x
345
     * @return boolean|string
346
     */
347
    protected function evaluateNot($x)
348
    {
349
        return !$x;
350
    }
351
352
    /**
353
     * Compare two variables based on a specified comparator
354
     *
355
     * @param mixed $x
356
     * @param mixed $y
357
     * @param string $comparator
358
     * @return boolean|string
359
     */
360
    protected function evaluateCompare($x, $y, $comparator)
361
    {
362
        // enfore strong comparison for comparing two objects
363
        if ($comparator == '==' && is_object($x) && is_object($y)) {
364
            $comparator = '===';
365
        }
366
        if ($comparator == '!=' && is_object($x) && is_object($y)) {
367
            $comparator = '!==';
368
        }
369
370
        if ($this->compileToCode === true) {
371
            return sprintf('(%s %s %s)', $x, $comparator, $y);
372
        }
373
374
        switch ($comparator) {
375
            case '==':
376
                $x = ($x == $y);
377
                break;
378
379
            case '===':
380
                $x = ($x === $y);
381
                break;
382
383
            case '!=':
384
                $x = ($x != $y);
385
                break;
386
387
            case '!==':
388
                $x = ($x !== $y);
389
                break;
390
391
            case '<=':
392
                $x = ($x <= $y);
393
                break;
394
395
            case '>=':
396
                $x = ($x >= $y);
397
                break;
398
399
            case '<':
400
                $x = ($x < $y);
401
                break;
402
403
            case '>':
404
                $x = ($x > $y);
405
                break;
406
407
            case '%':
408
                $x = ($x % $y);
409
                break;
410
        }
411
        return $x;
412
    }
413
414
    /**
415
     * Takes care of fetching terms from the context, converting to float/int,
416
     * converting true/false keywords into boolean or trim the final string of
417
     * quotation marks
418
     *
419
     * @param string $x
420
     * @param array $context
421
     * @return mixed
422
     */
423
    protected function evaluateTerm($x, $context)
424
    {
425
        if (isset($context[$x]) || (strpos($x, '{') === 0 && substr($x, -1) === '}')) {
426
            if ($this->compileToCode === true) {
427
                return '($context["' . trim($x, '{}') . '"])';
428
            }
429
            return $context[trim($x, '{}')];
430
        }
431
432
        if (is_numeric($x)) {
433
            if ($this->compileToCode === true) {
434
                return $x;
435
            }
436
            if (strpos($x, '.') !== false) {
437
                return floatval($x);
438
            } else {
439
                return intval($x);
440
            }
441
        }
442
443 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...
444
            if ($this->compileToCode === true) {
445
                return 'TRUE';
446
            }
447
            return true;
448
        }
449 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...
450
            if ($this->compileToCode === true) {
451
                return 'FALSE';
452
            }
453
            return false;
454
        }
455
456
        if ($this->compileToCode === true) {
457
            return '"' . trim($x, '\'"') . '"';
458
        }
459
460
        return trim($x, '\'"');
461
    }
462
}
463