Completed
Push — cleanup-service ( 87abfc...b0282b )
by Romain
05:55
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);
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