Completed
Branch conditions-refactoring (b9cd80)
by Romain
02:01
created

ConditionParser::getNewScope()   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
 * 2016 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\ConditionTree;
17
use Romm\Formz\Condition\Node\BooleanNode;
18
use Romm\Formz\Condition\Node\ConditionNode;
19
use Romm\Formz\Condition\Node\NodeInterface;
20
use Romm\Formz\Condition\Node\NullNode;
21
use Romm\Formz\Configuration\Form\Condition\Activation\ActivationInterface;
22
use Romm\Formz\Configuration\Form\Condition\Activation\EmptyActivation;
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
    const LOGICAL_AND = '&&';
51
    const LOGICAL_OR = '||';
52
53
    /**
54
     * @var Result
55
     */
56
    private $result;
57
58
    /**
59
     * @var ActivationInterface
60
     */
61
    private $condition;
62
63
    /**
64
     * @var ConditionParserScope
65
     */
66
    private $scope;
67
68
    /**
69
     * @return ConditionParser
70
     */
71
    public static function get()
72
    {
73
        return GeneralUtility::makeInstance(self::class);
74
    }
75
76
    /**
77
     * See class documentation.
78
     *
79
     * @param ActivationInterface $condition
80
     * @return ConditionTree
81
     */
82
    public function parse(ActivationInterface $condition)
83
    {
84
        $this->resetParser($condition);
85
86
        $rootNode = ($condition instanceof EmptyActivation)
87
            ? NullNode::get()
88
            : $this->getNodeRecursive();
89
90
        return GeneralUtility::makeInstance(ConditionTree::class, $rootNode, $this->result);
91
    }
92
93
    /**
94
     * @param ActivationInterface $condition
95
     */
96
    private function resetParser(ActivationInterface $condition)
97
    {
98
        $this->condition = $condition;
99
        $this->result = GeneralUtility::makeInstance(Result::class);
100
101
        $this->scope = $this->getNewScope();
102
        $this->scope->setExpression($this->splitConditionExpression($condition->getCondition()));
103
    }
104
105
    /**
106
     * Recursive function to convert an array of condition data to a nodes tree.
107
     *
108
     * @return NodeInterface|null
109
     */
110
    private function getNodeRecursive()
111
    {
112
        while (false === empty($this->scope->getExpression())) {
113
            if ($this->result->hasErrors()) {
114
                break;
115
            }
116
117
            $currentExpression = $this->scope->getExpression();
118
            $this->processToken($currentExpression[0]);
119
120
            if (null !== $this->scope->getCurrentLeftNode()
121
                && null !== $this->scope->getNode()
122
                && null !== $this->scope->getCurrentOperator()
123
            ) {
124
                $node = $this->getNode(
125
                    BooleanNode::class,
126
                    [$this->scope->getCurrentLeftNode(), $this->scope->getNode(), $this->scope->getCurrentOperator()]
127
                );
128
                $this->scope
129
                    ->setNode($node)
130
                    ->deleteCurrentLeftNode()
131
                    ->deleteCurrentOperator();
132
            }
133
        }
134
135
        if (null !== $this->scope->getCurrentLeftNode()) {
136
            $this->addError('Logical operator must be followed by a valid operation.', 1457545071);
137
        } elseif (null !== $this->scope->getLastOrNode()) {
138
            $node = $this->getNode(
139
                BooleanNode::class,
140
                [$this->scope->getLastOrNode(), $this->scope->getNode(), ConditionParser::LOGICAL_OR]
141
            );
142
            $this->scope->setNode($node);
143
        }
144
145
        $node = $this->scope->getNode();
146
        unset($this->scope);
147
148
        return $node;
149
    }
150
151
    /**
152
     * Will process a given token, which should be in the list of know tokens.
153
     *
154
     * @param string $token
155
     */
156
    private function processToken($token)
157
    {
158
        switch ($token) {
159
            case ')':
160
                $this->processTokenClosingParenthesis();
161
                break;
162
            case '(':
163
                $this->processTokenOpeningParenthesis();
164
                break;
165
            case ConditionParser::LOGICAL_OR:
166
            case ConditionParser::LOGICAL_AND:
167
                $this->processTokenLogicalOperator($token);
168
                break;
169
            default:
170
                $this->processTokenCondition($token);
171
                break;
172
        }
173
    }
174
175
    /**
176
     * Will process the opening parenthesis token `(`.
177
     *
178
     * A new scope will be created, containing the whole tokens list, which are
179
     * located between the opening parenthesis and the closing one. The scope
180
     * will be processed in a new scope, then the result is stored and the
181
     * process can keep up.
182
     */
183
    private function processTokenOpeningParenthesis()
184
    {
185
        $groupNode = $this->getGroupNode($this->scope->getExpression());
186
187
        $scopeSave = $this->scope;
188
        $expression = array_slice($scopeSave->getExpression(), count($groupNode) + 2);
189
        $scopeSave->setExpression($expression);
190
191
        $newScope = $this->getNewScope();
192
        $newScope->setExpression($groupNode);
193
194
        $this->scope = $newScope;
195
196
        $node = $this->getNodeRecursive();
197
198
        $this->scope = $scopeSave;
199
        $this->scope->setNode($node);
200
    }
201
202
    /**
203
     * Will process the closing parenthesis token `)`.
204
     *
205
     * This function should not be called, because the closing parenthesis
206
     * should always be handled by the opening parenthesis token handler.
207
     */
208
    private function processTokenClosingParenthesis()
209
    {
210
        $this->addError('Parenthesis closes invalid group.', 1457969163);
211
    }
212
213
    /**
214
     * Will process the logical operator tokens `&&` and `||`.
215
     *
216
     * Depending on the type of the operator, the process will change.
217
     *
218
     * @param string $operator
219
     */
220
    private function processTokenLogicalOperator($operator)
221
    {
222
        if (null === $this->scope->getNode()) {
223
            $this->addError('Logical operator must be preceded by a valid operation.', 1457544986);
224
        } else {
225
            if (ConditionParser::LOGICAL_OR === $operator) {
226
                if (null !== $this->scope->getLastOrNode()) {
227
                    /*
228
                     * If a `or` node was already registered, we create a new
229
                     * boolean node to join the two nodes.
230
                     */
231
                    $node = $this->getNode(
232
                        BooleanNode::class,
233
                        [$this->scope->getLastOrNode(), $this->scope->getNode(), $operator]
234
                    );
235
                    $this->scope->setNode($node);
236
                }
237
238
                $this->scope->setLastOrNode($this->scope->getNode());
239
            } else {
240
                $this->scope
241
                    ->setCurrentLeftNode($this->scope->getNode())
242
                    ->deleteNode();
243
            }
244
245
            $this->scope
246
                ->setCurrentOperator($operator)
247
                ->shiftExpression();
248
        }
249
    }
250
251
    /**
252
     * Will process the condition token.
253
     *
254
     * The condition must exist in the list of items of the condition.
255
     *
256
     * @param string $condition
257
     */
258
    private function processTokenCondition($condition)
259
    {
260
        if (false === $this->condition->hasItem($condition)) {
261
            $this->addError('The condition "' . $condition . '" does not exist.', 1457628378);
262
        } else {
263
            $node = $this->getNode(
264
                ConditionNode::class,
265
                [
266
                    $condition,
267
                    $this->condition->getItem($condition)
268
                ]
269
            );
270
            $this->scope
271
                ->setNode($node)
272
                ->shiftExpression();
273
        }
274
    }
275
276
    /**
277
     * @param string $nodeClassName
278
     * @param array  $arguments
279
     * @return NodeInterface
280
     */
281
    private function getNode($nodeClassName, array $arguments)
282
    {
283
        return call_user_func_array(
284
            [GeneralUtility::class, 'makeInstance'],
285
            array_merge([$nodeClassName], $arguments)
286
        );
287
    }
288
289
    /**
290
     * Will fetch a group of operations in a given array: the first item must be
291
     * a parenthesis. If its closing parenthesis is found, then the inner part
292
     * of the group is returned. Example:
293
     *
294
     * Input: (cond1 && (cond2 || cond3)) && cond4
295
     * Output: cond1 && (cond2 || cond3)
296
     *
297
     * @param array $expression
298
     * @return array
299
     */
300
    private function getGroupNode(array $expression)
301
    {
302
        $index = $this->getGroupNodeClosingIndex($expression);
303
        $finalSplitCondition = [];
304
305
        if (-1 === $index) {
306
            $this->addError('Parenthesis not correctly closed.', 1457544856);
307
        } else {
308
            for ($i = 1; $i < $index; $i++) {
309
                $finalSplitCondition[] = $expression[$i];
310
            }
311
        }
312
313
        return $finalSplitCondition;
314
    }
315
316
    /**
317
     * Returns the index of the closing parenthesis that matches the opening
318
     * parenthesis at index 0 of the given expression.
319
     *
320
     * @param array $expression
321
     * @return int
322
     */
323
    private function getGroupNodeClosingIndex(array $expression)
324
    {
325
        $parenthesis = 1;
326
        $index = 0;
327
328
        while ($parenthesis > 0) {
329
            $index++;
330
            if ($index > count($expression)) {
331
                $index = -1;
332
                break;
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
    private 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
    private function getNewScope()
363
    {
364
        return new ConditionParserScope;
365
    }
366
367
    /**
368
     * @param string $message
369
     * @param int    $code
370
     */
371
    private function addError($message, $code)
372
    {
373
        $error = new Error($message, $code);
374
        $this->result->addError($error);
375
    }
376
}
377