ConditionParser   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 328
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 37
lcom 1
cbo 8
dl 0
loc 328
rs 9.44
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A parse() 0 21 4
A resetParser() 0 8 1
A getNodeRecursive() 0 15 2
A processToken() 0 20 5
A processTokenOpeningParenthesis() 0 16 1
A processTokenClosingParenthesis() 0 4 1
A processTokenLogicalOperator() 0 27 4
A processTokenCondition() 0 11 2
A processLogicalAndNode() 0 15 4
A processLastLogicalOperatorNode() 0 11 3
A getGroupNode() 0 11 2
A getGroupNodeClosingIndex() 0 20 5
A splitConditionExpression() 0 6 1
A getNewScope() 0 4 1
A addError() 0 4 1
1
<?php
2
/*
3
 * 2017 Romain CANON <[email protected]>
4
 *
5
 * This file is part of the TYPO3 FormZ project.
6
 * It is free software; you can redistribute it and/or modify it
7
 * under the terms of the GNU General Public License, either
8
 * version 3 of the License, or any later version.
9
 *
10
 * For the full copyright and license information, see:
11
 * http://www.gnu.org/licenses/gpl-3.0.html
12
 */
13
14
namespace Romm\Formz\Condition\Parser;
15
16
use Romm\Formz\Condition\Exceptions\ConditionParserException;
17
use Romm\Formz\Condition\Parser\Node\BooleanNode;
18
use Romm\Formz\Condition\Parser\Node\ConditionNode;
19
use Romm\Formz\Condition\Parser\Node\NodeInterface;
20
use Romm\Formz\Condition\Parser\Node\NullNode;
21
use Romm\Formz\Configuration\Form\Field\Activation\ActivationInterface;
22
use Romm\Formz\Configuration\Form\Field\Activation\EmptyActivation;
23
use Romm\Formz\Service\Traits\SelfInstantiateTrait;
24
use TYPO3\CMS\Core\SingletonInterface;
25
use TYPO3\CMS\Core\Utility\GeneralUtility;
26
use TYPO3\CMS\Extbase\Error\Error;
27
use TYPO3\CMS\Extbase\Error\Result;
28
29
/**
30
 * A parser capable of parsing a validation condition string from a field
31
 * configuration, by creating a tree containing nodes that represent the
32
 * logical operations.
33
 *
34
 * Calling the function `parse()` will return an instance of `ConditionTree`
35
 * which will contain the full nodes tree, as well as a result instance.
36
 *
37
 * Parsing errors are handled, and stored in the tree result instance. When a
38
 * condition has been parsed, it is highly recommended to check if the result
39
 * contains errors before using the tree.
40
 *
41
 * Below is a list of what is currently supported by the parser:
42
 *  - Logical AND: defined by `&&`, it is no more than a logical "and" operator.
43
 *  - Logical OR: defined by `||`, same as above.
44
 *  - Operation groups: you can group several operation between parenthesis:
45
 *    `(...)`.
46
 *  - Condition names: represented by the items names in the `Activation`
47
 *    instance, there real meaning is a boolean result.
48
 */
49
class ConditionParser implements SingletonInterface
50
{
51
    use SelfInstantiateTrait;
52
53
    const LOGICAL_AND = '&&';
54
    const LOGICAL_OR = '||';
55
56
    const ERROR_CODE_INVALID_CLOSING_PARENTHESIS = 1457969163;
57
    const ERROR_CODE_CLOSING_PARENTHESIS_NOT_FOUND = 1457544856;
58
    const ERROR_CODE_CONDITION_NOT_FOUND = 1457628378;
59
    const ERROR_CODE_LOGICAL_OPERATOR_PRECEDED = 1457544986;
60
    const ERROR_CODE_LOGICAL_OPERATOR_FOLLOWED = 1457545071;
61
62
    /**
63
     * @var Result
64
     */
65
    protected $result;
66
67
    /**
68
     * @var ActivationInterface
69
     */
70
    protected $condition;
71
72
    /**
73
     * @var ConditionParserScope
74
     */
75
    protected $scope;
76
77
    /**
78
     * See class documentation.
79
     *
80
     * @param ActivationInterface $condition
81
     * @return ConditionTree
82
     */
83
    public function parse(ActivationInterface $condition)
84
    {
85
        $rootNode = null;
86
        $this->resetParser($condition);
87
88
        if (false === $condition instanceof EmptyActivation) {
89
            try {
90
                $rootNode = $this->getNodeRecursive();
91
            } catch (ConditionParserException $exception) {
92
                $error = new Error($exception->getMessage(), $exception->getCode());
93
                $this->result->addError($error);
94
            }
95
        }
96
97
        $rootNode = $rootNode ?: NullNode::get();
98
99
        /** @var ConditionTree $tree */
100
        $tree = GeneralUtility::makeInstance(ConditionTree::class, $rootNode, $this->result);
101
102
        return $tree;
103
    }
104
105
    /**
106
     * @param ActivationInterface $condition
107
     */
108
    protected function resetParser(ActivationInterface $condition)
109
    {
110
        $this->condition = $condition;
111
        $this->result = GeneralUtility::makeInstance(Result::class);
112
113
        $this->scope = $this->getNewScope();
114
        $this->scope->setExpression($this->splitConditionExpression($condition->getExpression()));
115
    }
116
117
    /**
118
     * Recursive function to convert an array of condition data to a nodes tree.
119
     *
120
     * @return NodeInterface|null
121
     */
122
    protected function getNodeRecursive()
123
    {
124
        while (false === empty($this->scope->getExpression())) {
125
            $currentExpression = $this->scope->getExpression();
126
            $this->processToken($currentExpression[0]);
127
            $this->processLogicalAndNode();
128
        }
129
130
        $this->processLastLogicalOperatorNode();
131
132
        $node = $this->scope->getNode();
133
        unset($this->scope);
134
135
        return $node;
136
    }
137
138
    /**
139
     * Will process a given token, which should be in the list of know tokens.
140
     *
141
     * @param string $token
142
     * @return $this
143
     */
144
    protected function processToken($token)
145
    {
146
        switch ($token) {
147
            case ')':
148
                $this->processTokenClosingParenthesis();
149
                break;
150
            case '(':
151
                $this->processTokenOpeningParenthesis();
152
                break;
153
            case self::LOGICAL_OR:
154
            case self::LOGICAL_AND:
155
                $this->processTokenLogicalOperator($token);
156
                break;
157
            default:
158
                $this->processTokenCondition($token);
159
                break;
160
        }
161
162
        return $this;
163
    }
164
165
    /**
166
     * Will process the opening parenthesis token `(`.
167
     *
168
     * A new scope will be created, containing the whole tokens list, which are
169
     * located between the opening parenthesis and the closing one. The scope
170
     * will be processed in a new scope, then the result is stored and the
171
     * process can keep up.
172
     */
173
    protected function processTokenOpeningParenthesis()
174
    {
175
        $groupNode = $this->getGroupNode($this->scope->getExpression());
176
177
        $scopeSave = $this->scope;
178
        $expression = array_slice($scopeSave->getExpression(), count($groupNode) + 2);
179
        $scopeSave->setExpression($expression);
180
181
        $this->scope = $this->getNewScope();
182
        $this->scope->setExpression($groupNode);
183
184
        $node = $this->getNodeRecursive();
185
186
        $this->scope = $scopeSave;
187
        $this->scope->setNode($node);
0 ignored issues
show
Bug introduced by
It seems like $node defined by $this->getNodeRecursive() on line 184 can be null; however, Romm\Formz\Condition\Par...nParserScope::setNode() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
188
    }
189
190
    /**
191
     * Will process the closing parenthesis token `)`.
192
     *
193
     * This function should not be called, because the closing parenthesis
194
     * should always be handled by the opening parenthesis token handler.
195
     */
196
    protected function processTokenClosingParenthesis()
197
    {
198
        $this->addError('Parenthesis closes invalid group.', self::ERROR_CODE_INVALID_CLOSING_PARENTHESIS);
199
    }
200
201
    /**
202
     * Will process the logical operator tokens `&&` and `||`.
203
     *
204
     * Depending on the type of the operator, the process will change.
205
     *
206
     * @param string $operator
207
     */
208
    protected function processTokenLogicalOperator($operator)
209
    {
210
        if (null === $this->scope->getNode()) {
211
            $this->addError('Logical operator must be preceded by a valid operation.', self::ERROR_CODE_LOGICAL_OPERATOR_PRECEDED);
212
        } else {
213
            if (self::LOGICAL_OR === $operator) {
214
                if (null !== $this->scope->getLastOrNode()) {
215
                    /*
216
                     * If a `or` node was already registered, we create a new
217
                     * boolean node to join the two nodes.
218
                     */
219
                    $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), $operator);
220
                    $this->scope->setNode($node);
221
                }
222
223
                $this->scope->setLastOrNode($this->scope->getNode());
224
            } else {
225
                $this->scope
226
                    ->setCurrentLeftNode($this->scope->getNode())
227
                    ->deleteNode();
228
            }
229
230
            $this->scope
231
                ->setCurrentOperator($operator)
232
                ->shiftExpression();
233
        }
234
    }
235
236
    /**
237
     * Will process the condition token.
238
     *
239
     * The condition must exist in the list of items of the condition.
240
     *
241
     * @param string $condition
242
     */
243
    protected function processTokenCondition($condition)
244
    {
245
        if (false === $this->condition->hasCondition($condition)) {
246
            $this->addError('The condition "' . $condition . '" does not exist.', self::ERROR_CODE_CONDITION_NOT_FOUND);
247
        } else {
248
            $node = new ConditionNode($condition, $this->condition->getCondition($condition));
249
            $this->scope
250
                ->setNode($node)
251
                ->shiftExpression();
252
        }
253
    }
254
255
    /**
256
     * Will check if a "logical and" node should be created, depending on which
257
     * tokens were processed before.
258
     *
259
     * @return $this
260
     */
261
    protected function processLogicalAndNode()
262
    {
263
        if (null !== $this->scope->getCurrentLeftNode()
264
            && null !== $this->scope->getNode()
265
            && null !== $this->scope->getCurrentOperator()
266
        ) {
267
            $node = new BooleanNode($this->scope->getCurrentLeftNode(), $this->scope->getNode(), $this->scope->getCurrentOperator());
268
            $this->scope
269
                ->setNode($node)
270
                ->deleteCurrentLeftNode()
271
                ->deleteCurrentOperator();
272
        }
273
274
        return $this;
275
    }
276
277
    /**
278
     * Will check if a last logical operator node is remaining.
279
     *
280
     * @return $this
281
     */
282
    protected function processLastLogicalOperatorNode()
283
    {
284
        if (null !== $this->scope->getCurrentLeftNode()) {
285
            $this->addError('Logical operator must be followed by a valid operation.', self::ERROR_CODE_LOGICAL_OPERATOR_FOLLOWED);
286
        } elseif (null !== $this->scope->getLastOrNode()) {
287
            $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), self::LOGICAL_OR);
288
            $this->scope->setNode($node);
289
        }
290
291
        return $this;
292
    }
293
294
    /**
295
     * Will fetch a group of operations in a given array: the first item must be
296
     * a parenthesis. If its closing parenthesis is found, then the inner part
297
     * of the group is returned. Example:
298
     *
299
     * Input: (cond1 && (cond2 || cond3)) && cond4
300
     * Output: cond1 && (cond2 || cond3)
301
     *
302
     * @param array $expression
303
     * @return array
304
     */
305
    protected function getGroupNode(array $expression)
306
    {
307
        $index = $this->getGroupNodeClosingIndex($expression);
308
        $finalSplitCondition = [];
309
310
        for ($i = 1; $i < $index; $i++) {
311
            $finalSplitCondition[] = $expression[$i];
312
        }
313
314
        return $finalSplitCondition;
315
    }
316
317
    /**
318
     * Returns the index of the closing parenthesis that matches the opening
319
     * parenthesis at index 0 of the given expression.
320
     *
321
     * @param array $expression
322
     * @return int
323
     */
324
    protected function getGroupNodeClosingIndex(array $expression)
325
    {
326
        $parenthesis = 1;
327
        $index = 0;
328
329
        while ($parenthesis > 0) {
330
            $index++;
331
            if ($index > count($expression)) {
332
                $this->addError('Parenthesis not correctly closed.', self::ERROR_CODE_CLOSING_PARENTHESIS_NOT_FOUND);
333
            }
334
335
            if ('(' === $expression[$index]) {
336
                $parenthesis++;
337
            } elseif (')' === $expression[$index]) {
338
                $parenthesis--;
339
            }
340
        }
341
342
        return $index;
343
    }
344
345
    /**
346
     * Will split a condition expression string in an exploded array where each
347
     * entry represents an operation.
348
     *
349
     * @param string $condition
350
     * @return array
351
     */
352
    protected function splitConditionExpression($condition)
353
    {
354
        preg_match_all('/(\w+|\(|\)|\&\&|\|\|)/', trim($condition), $result);
355
356
        return $result[0];
357
    }
358
359
    /**
360
     * @return ConditionParserScope
361
     */
362
    protected function getNewScope()
363
    {
364
        return new ConditionParserScope;
365
    }
366
367
    /**
368
     * @param string $message
369
     * @param int    $code
370
     * @throws ConditionParserException
371
     */
372
    protected function addError($message, $code)
373
    {
374
        throw new ConditionParserException($message, $code);
375
    }
376
}
377