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