Completed
Push — unit-tests-validation ( 0b3fd7...636d27 )
by Romain
02:29
created

ConditionParser::getNode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
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\Parser\Node\BooleanNode;
17
use Romm\Formz\Condition\Parser\Node\ConditionNode;
18
use Romm\Formz\Condition\Parser\Node\NodeInterface;
19
use Romm\Formz\Condition\Parser\Node\NullNode;
20
use Romm\Formz\Configuration\Form\Condition\Activation\ActivationInterface;
21
use Romm\Formz\Configuration\Form\Condition\Activation\EmptyActivation;
22
use Romm\Formz\Service\Traits\FacadeInstanceTrait;
23
use TYPO3\CMS\Core\SingletonInterface;
24
use TYPO3\CMS\Core\Utility\GeneralUtility;
25
use TYPO3\CMS\Extbase\Error\Error;
26
use TYPO3\CMS\Extbase\Error\Result;
27
28
/**
29
 * A parser capable of parsing a validation condition string from a field
30
 * configuration, by creating a tree containing nodes which represents the
31
 * logical operations.
32
 *
33
 * Calling the function `parse()` will return an instance of `ConditionTree`
34
 * which will contain the full nodes tree, as well as a result instance.
35
 *
36
 * Parsing errors are handled, and stored in the tree result instance. When a
37
 * condition has been parsed, it is highly recommended to check if the result
38
 * contains errors before using the tree.
39
 *
40
 * Below is a list of what is currently supported by the parser:
41
 *  - Logical AND: defined by `&&`, it is no more than a logical "and" operator.
42
 *  - Logical OR: defined by `||`, same as above.
43
 *  - Operation groups: you can group several operation between parenthesis:
44
 *    `(...)`.
45
 *  - Condition names: represented by the items names in the `Activation`
46
 *    instance, there real meaning is a boolean result.
47
 */
48
class ConditionParser implements SingletonInterface
49
{
50
    use FacadeInstanceTrait;
51
52
    const LOGICAL_AND = '&&';
53
    const LOGICAL_OR = '||';
54
55
    /**
56
     * @var Result
57
     */
58
    private $result;
59
60
    /**
61
     * @var ActivationInterface
62
     */
63
    private $condition;
64
65
    /**
66
     * @var ConditionParserScope
67
     */
68
    private $scope;
69
70
    /**
71
     * See class documentation.
72
     *
73
     * @param ActivationInterface $condition
74
     * @return ConditionTree
75
     */
76
    public function parse(ActivationInterface $condition)
77
    {
78
        $this->resetParser($condition);
79
80
        $rootNode = ($condition instanceof EmptyActivation)
81
            ? NullNode::get()
82
            : $this->getNodeRecursive();
83
84
        return GeneralUtility::makeInstance(ConditionTree::class, $rootNode, $this->result);
85
    }
86
87
    /**
88
     * @param ActivationInterface $condition
89
     */
90
    private function resetParser(ActivationInterface $condition)
91
    {
92
        $this->condition = $condition;
93
        $this->result = GeneralUtility::makeInstance(Result::class);
94
95
        $this->scope = $this->getNewScope();
96
        $this->scope->setExpression($this->splitConditionExpression($condition->getCondition()));
97
    }
98
99
    /**
100
     * Recursive function to convert an array of condition data to a nodes tree.
101
     *
102
     * @return NodeInterface|null
103
     */
104
    private function getNodeRecursive()
105
    {
106
        while (false === empty($this->scope->getExpression())) {
107
            if ($this->result->hasErrors()) {
108
                break;
109
            }
110
111
            $currentExpression = $this->scope->getExpression();
112
            $this->processToken($currentExpression[0]);
113
            $this->processLogicalAndNode();
114
        }
115
116
        $this->processLastLogicalOperatorNode();
117
118
        $node = $this->scope->getNode();
119
        unset($this->scope);
120
121
        return $node;
122
    }
123
124
    /**
125
     * Will process a given token, which should be in the list of know tokens.
126
     *
127
     * @param string $token
128
     * @return $this
129
     */
130
    private function processToken($token)
131
    {
132
        switch ($token) {
133
            case ')':
134
                $this->processTokenClosingParenthesis();
135
                break;
136
            case '(':
137
                $this->processTokenOpeningParenthesis();
138
                break;
139
            case self::LOGICAL_OR:
140
            case self::LOGICAL_AND:
141
                $this->processTokenLogicalOperator($token);
142
                break;
143
            default:
144
                $this->processTokenCondition($token);
145
                break;
146
        }
147
148
        return $this;
149
    }
150
151
    /**
152
     * Will process the opening parenthesis token `(`.
153
     *
154
     * A new scope will be created, containing the whole tokens list, which are
155
     * located between the opening parenthesis and the closing one. The scope
156
     * will be processed in a new scope, then the result is stored and the
157
     * process can keep up.
158
     */
159
    private function processTokenOpeningParenthesis()
160
    {
161
        $groupNode = $this->getGroupNode($this->scope->getExpression());
162
163
        $scopeSave = $this->scope;
164
        $expression = array_slice($scopeSave->getExpression(), count($groupNode) + 2);
165
        $scopeSave->setExpression($expression);
166
167
        $this->scope = $this->getNewScope();
168
        $this->scope->setExpression($groupNode);
169
170
        $node = $this->getNodeRecursive();
171
172
        $this->scope = $scopeSave;
173
        $this->scope->setNode($node);
0 ignored issues
show
Bug introduced by
It seems like $node defined by $this->getNodeRecursive() on line 170 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...
174
    }
175
176
    /**
177
     * Will process the closing parenthesis token `)`.
178
     *
179
     * This function should not be called, because the closing parenthesis
180
     * should always be handled by the opening parenthesis token handler.
181
     */
182
    private function processTokenClosingParenthesis()
183
    {
184
        $this->addError('Parenthesis closes invalid group.', 1457969163);
185
    }
186
187
    /**
188
     * Will process the logical operator tokens `&&` and `||`.
189
     *
190
     * Depending on the type of the operator, the process will change.
191
     *
192
     * @param string $operator
193
     */
194
    private function processTokenLogicalOperator($operator)
195
    {
196
        if (null === $this->scope->getNode()) {
197
            $this->addError('Logical operator must be preceded by a valid operation.', 1457544986);
198
        } else {
199
            if (self::LOGICAL_OR === $operator) {
200
                if (null !== $this->scope->getLastOrNode()) {
201
                    /*
202
                     * If a `or` node was already registered, we create a new
203
                     * boolean node to join the two nodes.
204
                     */
205
                    $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), $operator);
206
                    $this->scope->setNode($node);
207
                }
208
209
                $this->scope->setLastOrNode($this->scope->getNode());
210
            } else {
211
                $this->scope
212
                    ->setCurrentLeftNode($this->scope->getNode())
213
                    ->deleteNode();
214
            }
215
216
            $this->scope
217
                ->setCurrentOperator($operator)
218
                ->shiftExpression();
219
        }
220
    }
221
222
    /**
223
     * Will process the condition token.
224
     *
225
     * The condition must exist in the list of items of the condition.
226
     *
227
     * @param string $condition
228
     */
229
    private function processTokenCondition($condition)
230
    {
231
        if (false === $this->condition->hasItem($condition)) {
232
            $this->addError('The condition "' . $condition . '" does not exist.', 1457628378);
233
        } else {
234
            $node = new ConditionNode($condition, $this->condition->getItem($condition));
235
            $this->scope
236
                ->setNode($node)
237
                ->shiftExpression();
238
        }
239
    }
240
241
    /**
242
     * Will check if a "logical and" node should be created, depending on which
243
     * tokens were processed before.
244
     *
245
     * @return $this
246
     */
247
    private function processLogicalAndNode()
248
    {
249
        if (null !== $this->scope->getCurrentLeftNode()
250
            && null !== $this->scope->getNode()
251
            && null !== $this->scope->getCurrentOperator()
252
        ) {
253
            $node = new BooleanNode($this->scope->getCurrentLeftNode(), $this->scope->getNode(), $this->scope->getCurrentOperator());
254
            $this->scope
255
                ->setNode($node)
256
                ->deleteCurrentLeftNode()
257
                ->deleteCurrentOperator();
258
        }
259
260
        return $this;
261
    }
262
263
    /**
264
     * Will check if a last logical operator node is remaining.
265
     *
266
     * @return $this
267
     */
268
    private function processLastLogicalOperatorNode()
269
    {
270
        if (null !== $this->scope->getCurrentLeftNode()) {
271
            $this->addError('Logical operator must be followed by a valid operation.', 1457545071);
272
        } elseif (null !== $this->scope->getLastOrNode()) {
273
            $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), self::LOGICAL_OR);
274
            $this->scope->setNode($node);
275
        }
276
277
        return $this;
278
    }
279
280
    /**
281
     * Will fetch a group of operations in a given array: the first item must be
282
     * a parenthesis. If its closing parenthesis is found, then the inner part
283
     * of the group is returned. Example:
284
     *
285
     * Input: (cond1 && (cond2 || cond3)) && cond4
286
     * Output: cond1 && (cond2 || cond3)
287
     *
288
     * @param array $expression
289
     * @return array
290
     */
291
    private function getGroupNode(array $expression)
292
    {
293
        $index = $this->getGroupNodeClosingIndex($expression);
294
        $finalSplitCondition = [];
295
296
        if (-1 === $index) {
297
            $this->addError('Parenthesis not correctly closed.', 1457544856);
298
        } else {
299
            for ($i = 1; $i < $index; $i++) {
300
                $finalSplitCondition[] = $expression[$i];
301
            }
302
        }
303
304
        return $finalSplitCondition;
305
    }
306
307
    /**
308
     * Returns the index of the closing parenthesis that matches the opening
309
     * parenthesis at index 0 of the given expression.
310
     *
311
     * @param array $expression
312
     * @return int
313
     */
314
    private function getGroupNodeClosingIndex(array $expression)
315
    {
316
        $parenthesis = 1;
317
        $index = 0;
318
319
        while ($parenthesis > 0) {
320
            $index++;
321
            if ($index > count($expression)) {
322
                $index = -1;
323
                break;
324
            }
325
326
            if ('(' === $expression[$index]) {
327
                $parenthesis++;
328
            } elseif (')' === $expression[$index]) {
329
                $parenthesis--;
330
            }
331
        }
332
333
        return $index;
334
    }
335
336
    /**
337
     * Will split a condition expression string in an exploded array where each
338
     * entry represents an operation.
339
     *
340
     * @param string $condition
341
     * @return array
342
     */
343
    private function splitConditionExpression($condition)
344
    {
345
        preg_match_all('/(\w+|\(|\)|\&\&|\|\|)/', trim($condition), $result);
346
347
        return $result[0];
348
    }
349
350
    /**
351
     * @return ConditionParserScope
352
     */
353
    private function getNewScope()
354
    {
355
        return new ConditionParserScope;
356
    }
357
358
    /**
359
     * @param string $message
360
     * @param int    $code
361
     */
362
    private function addError($message, $code)
363
    {
364
        $error = new Error($message, $code);
365
        $this->result->addError($error);
366
    }
367
}
368