Completed
Push — typo3-v8-compatibility ( ae4db2...381377 )
by Romain
02:16
created

ConditionParser::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
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 = $this->getNode(
206
                        BooleanNode::class,
207
                        [$this->scope->getLastOrNode(), $this->scope->getNode(), $operator]
208
                    );
209
                    $this->scope->setNode($node);
210
                }
211
212
                $this->scope->setLastOrNode($this->scope->getNode());
213
            } else {
214
                $this->scope
215
                    ->setCurrentLeftNode($this->scope->getNode())
216
                    ->deleteNode();
217
            }
218
219
            $this->scope
220
                ->setCurrentOperator($operator)
221
                ->shiftExpression();
222
        }
223
    }
224
225
    /**
226
     * Will process the condition token.
227
     *
228
     * The condition must exist in the list of items of the condition.
229
     *
230
     * @param string $condition
231
     */
232
    private function processTokenCondition($condition)
233
    {
234
        if (false === $this->condition->hasItem($condition)) {
235
            $this->addError('The condition "' . $condition . '" does not exist.', 1457628378);
236
        } else {
237
            $node = $this->getNode(
238
                ConditionNode::class,
239
                [
240
                    $condition,
241
                    $this->condition->getItem($condition)
242
                ]
243
            );
244
            $this->scope
245
                ->setNode($node)
246
                ->shiftExpression();
247
        }
248
    }
249
250
    /**
251
     * Will check if a "logical and" node should be created, depending on which
252
     * tokens were processed before.
253
     *
254
     * @return $this
255
     */
256
    private function processLogicalAndNode()
257
    {
258
        if (null !== $this->scope->getCurrentLeftNode()
259
            && null !== $this->scope->getNode()
260
            && null !== $this->scope->getCurrentOperator()
261
        ) {
262
            $node = $this->getNode(
263
                BooleanNode::class,
264
                [$this->scope->getCurrentLeftNode(), $this->scope->getNode(), $this->scope->getCurrentOperator()]
265
            );
266
            $this->scope
267
                ->setNode($node)
268
                ->deleteCurrentLeftNode()
269
                ->deleteCurrentOperator();
270
        }
271
272
        return $this;
273
    }
274
275
    /**
276
     * Will check if a last logical operator node is remaining.
277
     *
278
     * @return $this
279
     */
280
    private function processLastLogicalOperatorNode()
281
    {
282
        if (null !== $this->scope->getCurrentLeftNode()) {
283
            $this->addError('Logical operator must be followed by a valid operation.', 1457545071);
284
        } elseif (null !== $this->scope->getLastOrNode()) {
285
            $node = $this->getNode(
286
                BooleanNode::class,
287
                [$this->scope->getLastOrNode(), $this->scope->getNode(), self::LOGICAL_OR]
288
            );
289
            $this->scope->setNode($node);
290
        }
291
292
        return $this;
293
    }
294
295
    /**
296
     * @param string $nodeClassName
297
     * @param array  $arguments
298
     * @return NodeInterface
299
     */
300
    private function getNode($nodeClassName, array $arguments)
301
    {
302
        return call_user_func_array(
303
            [GeneralUtility::class, 'makeInstance'],
304
            array_merge([$nodeClassName], $arguments)
305
        );
306
    }
307
308
    /**
309
     * Will fetch a group of operations in a given array: the first item must be
310
     * a parenthesis. If its closing parenthesis is found, then the inner part
311
     * of the group is returned. Example:
312
     *
313
     * Input: (cond1 && (cond2 || cond3)) && cond4
314
     * Output: cond1 && (cond2 || cond3)
315
     *
316
     * @param array $expression
317
     * @return array
318
     */
319
    private function getGroupNode(array $expression)
320
    {
321
        $index = $this->getGroupNodeClosingIndex($expression);
322
        $finalSplitCondition = [];
323
324
        if (-1 === $index) {
325
            $this->addError('Parenthesis not correctly closed.', 1457544856);
326
        } else {
327
            for ($i = 1; $i < $index; $i++) {
328
                $finalSplitCondition[] = $expression[$i];
329
            }
330
        }
331
332
        return $finalSplitCondition;
333
    }
334
335
    /**
336
     * Returns the index of the closing parenthesis that matches the opening
337
     * parenthesis at index 0 of the given expression.
338
     *
339
     * @param array $expression
340
     * @return int
341
     */
342
    private function getGroupNodeClosingIndex(array $expression)
343
    {
344
        $parenthesis = 1;
345
        $index = 0;
346
347
        while ($parenthesis > 0) {
348
            $index++;
349
            if ($index > count($expression)) {
350
                $index = -1;
351
                break;
352
            }
353
354
            if ('(' === $expression[$index]) {
355
                $parenthesis++;
356
            } elseif (')' === $expression[$index]) {
357
                $parenthesis--;
358
            }
359
        }
360
361
        return $index;
362
    }
363
364
    /**
365
     * Will split a condition expression string in an exploded array where each
366
     * entry represents an operation.
367
     *
368
     * @param string $condition
369
     * @return array
370
     */
371
    private function splitConditionExpression($condition)
372
    {
373
        preg_match_all('/(\w+|\(|\)|\&\&|\|\|)/', trim($condition), $result);
374
375
        return $result[0];
376
    }
377
378
    /**
379
     * @return ConditionParserScope
380
     */
381
    private function getNewScope()
382
    {
383
        return new ConditionParserScope;
384
    }
385
386
    /**
387
     * @param string $message
388
     * @param int    $code
389
     */
390
    private function addError($message, $code)
391
    {
392
        $error = new Error($message, $code);
393
        $this->result->addError($error);
394
    }
395
}
396