Completed
Push — unit-test-form-view-helper ( 21ce8b...04fc41 )
by Romain
04:37
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
            ? null
82
            : $this->getNodeRecursive();
83
84
        $rootNode = $rootNode ?: NullNode::get();
85
86
        return GeneralUtility::makeInstance(ConditionTree::class, $rootNode, $this->result);
87
    }
88
89
    /**
90
     * @param ActivationInterface $condition
91
     */
92
    private function resetParser(ActivationInterface $condition)
93
    {
94
        $this->condition = $condition;
95
        $this->result = GeneralUtility::makeInstance(Result::class);
96
97
        $this->scope = $this->getNewScope();
98
        $this->scope->setExpression($this->splitConditionExpression($condition->getCondition()));
99
    }
100
101
    /**
102
     * Recursive function to convert an array of condition data to a nodes tree.
103
     *
104
     * @return NodeInterface|null
105
     */
106
    private function getNodeRecursive()
107
    {
108
        while (false === empty($this->scope->getExpression())) {
109
            if ($this->result->hasErrors()) {
110
                break;
111
            }
112
113
            $currentExpression = $this->scope->getExpression();
114
            $this->processToken($currentExpression[0]);
115
            $this->processLogicalAndNode();
116
        }
117
118
        $this->processLastLogicalOperatorNode();
119
120
        $node = $this->scope->getNode();
121
        unset($this->scope);
122
123
        return $node;
124
    }
125
126
    /**
127
     * Will process a given token, which should be in the list of know tokens.
128
     *
129
     * @param string $token
130
     * @return $this
131
     */
132
    private function processToken($token)
133
    {
134
        switch ($token) {
135
            case ')':
136
                $this->processTokenClosingParenthesis();
137
                break;
138
            case '(':
139
                $this->processTokenOpeningParenthesis();
140
                break;
141
            case self::LOGICAL_OR:
142
            case self::LOGICAL_AND:
143
                $this->processTokenLogicalOperator($token);
144
                break;
145
            default:
146
                $this->processTokenCondition($token);
147
                break;
148
        }
149
150
        return $this;
151
    }
152
153
    /**
154
     * Will process the opening parenthesis token `(`.
155
     *
156
     * A new scope will be created, containing the whole tokens list, which are
157
     * located between the opening parenthesis and the closing one. The scope
158
     * will be processed in a new scope, then the result is stored and the
159
     * process can keep up.
160
     */
161
    private function processTokenOpeningParenthesis()
162
    {
163
        $groupNode = $this->getGroupNode($this->scope->getExpression());
164
165
        $scopeSave = $this->scope;
166
        $expression = array_slice($scopeSave->getExpression(), count($groupNode) + 2);
167
        $scopeSave->setExpression($expression);
168
169
        $this->scope = $this->getNewScope();
170
        $this->scope->setExpression($groupNode);
171
172
        $node = $this->getNodeRecursive();
173
174
        $this->scope = $scopeSave;
175
        $this->scope->setNode($node);
0 ignored issues
show
Bug introduced by
It seems like $node defined by $this->getNodeRecursive() on line 172 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...
176
    }
177
178
    /**
179
     * Will process the closing parenthesis token `)`.
180
     *
181
     * This function should not be called, because the closing parenthesis
182
     * should always be handled by the opening parenthesis token handler.
183
     */
184
    private function processTokenClosingParenthesis()
185
    {
186
        $this->addError('Parenthesis closes invalid group.', 1457969163);
187
    }
188
189
    /**
190
     * Will process the logical operator tokens `&&` and `||`.
191
     *
192
     * Depending on the type of the operator, the process will change.
193
     *
194
     * @param string $operator
195
     */
196
    private function processTokenLogicalOperator($operator)
197
    {
198
        if (null === $this->scope->getNode()) {
199
            $this->addError('Logical operator must be preceded by a valid operation.', 1457544986);
200
        } else {
201
            if (self::LOGICAL_OR === $operator) {
202
                if (null !== $this->scope->getLastOrNode()) {
203
                    /*
204
                     * If a `or` node was already registered, we create a new
205
                     * boolean node to join the two nodes.
206
                     */
207
                    $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), $operator);
208
                    $this->scope->setNode($node);
209
                }
210
211
                $this->scope->setLastOrNode($this->scope->getNode());
212
            } else {
213
                $this->scope
214
                    ->setCurrentLeftNode($this->scope->getNode())
215
                    ->deleteNode();
216
            }
217
218
            $this->scope
219
                ->setCurrentOperator($operator)
220
                ->shiftExpression();
221
        }
222
    }
223
224
    /**
225
     * Will process the condition token.
226
     *
227
     * The condition must exist in the list of items of the condition.
228
     *
229
     * @param string $condition
230
     */
231
    private function processTokenCondition($condition)
232
    {
233
        if (false === $this->condition->hasItem($condition)) {
234
            $this->addError('The condition "' . $condition . '" does not exist.', 1457628378);
235
        } else {
236
            $node = new ConditionNode($condition, $this->condition->getItem($condition));
237
            $this->scope
238
                ->setNode($node)
239
                ->shiftExpression();
240
        }
241
    }
242
243
    /**
244
     * Will check if a "logical and" node should be created, depending on which
245
     * tokens were processed before.
246
     *
247
     * @return $this
248
     */
249
    private function processLogicalAndNode()
250
    {
251
        if (null !== $this->scope->getCurrentLeftNode()
252
            && null !== $this->scope->getNode()
253
            && null !== $this->scope->getCurrentOperator()
254
        ) {
255
            $node = new BooleanNode($this->scope->getCurrentLeftNode(), $this->scope->getNode(), $this->scope->getCurrentOperator());
256
            $this->scope
257
                ->setNode($node)
258
                ->deleteCurrentLeftNode()
259
                ->deleteCurrentOperator();
260
        }
261
262
        return $this;
263
    }
264
265
    /**
266
     * Will check if a last logical operator node is remaining.
267
     *
268
     * @return $this
269
     */
270
    private function processLastLogicalOperatorNode()
271
    {
272
        if (null !== $this->scope->getCurrentLeftNode()) {
273
            $this->addError('Logical operator must be followed by a valid operation.', 1457545071);
274
        } elseif (null !== $this->scope->getLastOrNode()) {
275
            $node = new BooleanNode($this->scope->getLastOrNode(), $this->scope->getNode(), self::LOGICAL_OR);
276
            $this->scope->setNode($node);
277
        }
278
279
        return $this;
280
    }
281
282
    /**
283
     * Will fetch a group of operations in a given array: the first item must be
284
     * a parenthesis. If its closing parenthesis is found, then the inner part
285
     * of the group is returned. Example:
286
     *
287
     * Input: (cond1 && (cond2 || cond3)) && cond4
288
     * Output: cond1 && (cond2 || cond3)
289
     *
290
     * @param array $expression
291
     * @return array
292
     */
293
    private function getGroupNode(array $expression)
294
    {
295
        $index = $this->getGroupNodeClosingIndex($expression);
296
        $finalSplitCondition = [];
297
298
        if (-1 === $index) {
299
            $this->addError('Parenthesis not correctly closed.', 1457544856);
300
        } else {
301
            for ($i = 1; $i < $index; $i++) {
302
                $finalSplitCondition[] = $expression[$i];
303
            }
304
        }
305
306
        return $finalSplitCondition;
307
    }
308
309
    /**
310
     * Returns the index of the closing parenthesis that matches the opening
311
     * parenthesis at index 0 of the given expression.
312
     *
313
     * @param array $expression
314
     * @return int
315
     */
316
    private function getGroupNodeClosingIndex(array $expression)
317
    {
318
        $parenthesis = 1;
319
        $index = 0;
320
321
        while ($parenthesis > 0) {
322
            $index++;
323
            if ($index > count($expression)) {
324
                $index = -1;
325
                break;
326
            }
327
328
            if ('(' === $expression[$index]) {
329
                $parenthesis++;
330
            } elseif (')' === $expression[$index]) {
331
                $parenthesis--;
332
            }
333
        }
334
335
        return $index;
336
    }
337
338
    /**
339
     * Will split a condition expression string in an exploded array where each
340
     * entry represents an operation.
341
     *
342
     * @param string $condition
343
     * @return array
344
     */
345
    private function splitConditionExpression($condition)
346
    {
347
        preg_match_all('/(\w+|\(|\)|\&\&|\|\|)/', trim($condition), $result);
348
349
        return $result[0];
350
    }
351
352
    /**
353
     * @return ConditionParserScope
354
     */
355
    private function getNewScope()
356
    {
357
        return new ConditionParserScope;
358
    }
359
360
    /**
361
     * @param string $message
362
     * @param int    $code
363
     */
364
    private function addError($message, $code)
365
    {
366
        $error = new Error($message, $code);
367
        $this->result->addError($error);
368
    }
369
}
370