Completed
Branch conditions-refactoring (4b1dba)
by Romain
03:17
created

ConditionParser::processTokenLogicalOperator()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 18
nc 4
nop 1
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
 * @todo
30
 *
31
 * A parser capable of parsing a validation condition string from a field
32
 * configuration, by creating a tree containing nodes which represents the
33
 * logical operations.
34
 *
35
 * Parsing errors are handled, and stored in `$this->result`. When a condition
36
 * has been parser, it is highly recommended to check if the result contains
37
 * 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 ConditionParserState
64
     */
65
    private $state;
66
67
    /**
68
     * @return ConditionParser
69
     */
70
    public static function get()
71
    {
72
        return GeneralUtility::makeInstance(self::class);
73
    }
74
75
    /**
76
     * @param ActivationInterface $condition
77
     * @return ConditionTree
78
     */
79
    public function parse(ActivationInterface $condition)
80
    {
81
        $this->resetParser($condition);
82
83
        $rootNode = ($condition instanceof EmptyActivation)
84
            ? NullNode::get()
85
            : $this->getNodeRecursive();
86
87
        return GeneralUtility::makeInstance(ConditionTree::class, $rootNode, $this->result);
88
    }
89
90
    /**
91
     * @param ActivationInterface $condition
92
     */
93
    private function resetParser(ActivationInterface $condition)
94
    {
95
        $this->condition = $condition;
96
        $this->result = GeneralUtility::makeInstance(Result::class);
97
98
        $this->state = $this->getNewState();
99
        $this->state->setCurrentExpression($this->splitConditionExpression($condition->getCondition()));
100
    }
101
102
    /**
103
     * Recursive function to convert an array of condition data to a tree of
104
     * nodes.
105
     *
106
     * @return NodeInterface|null
107
     */
108
    protected function getNodeRecursive()
109
    {
110
        while (false === empty($this->state->getCurrentExpression())) {
111
            if ($this->result->hasErrors()) {
112
                break;
113
            }
114
115
            $currentExpression = $this->state->getCurrentExpression();
116
            $this->processToken($currentExpression[0]);
117
118
            if (null !== $this->state->getCurrentLeftNode()
119
                && null !== $this->state->getCurrentNode()
120
                && null !== $this->state->getCurrentOperator()
121
            ) {
122
                $node = $this->getNode(
123
                    BooleanNode::class,
124
                    [$this->state->getCurrentLeftNode(), $this->state->getCurrentNode(), $this->state->getCurrentOperator()]
125
                );
126
                $this->state
127
                    ->setCurrentNode($node)
128
                    ->deleteCurrentLeftNode()
129
                    ->deleteCurrentOperator();
130
            }
131
        }
132
133
        if (null !== $this->state->getCurrentLeftNode()) {
134
            $this->addError('Logical operator must be followed by a valid operation.', 1457545071);
135
        } elseif (null !== $this->state->getLastOrNode()) {
136
            $node = $this->getNode(
137
                BooleanNode::class,
138
                [$this->state->getLastOrNode(), $this->state->getCurrentNode(), ConditionParser::LOGICAL_OR]
139
            );
140
            $this->state->setCurrentNode($node);
141
        }
142
143
        return $this->state->getCurrentNode();
144
    }
145
146
    protected function processToken($token)
147
    {
148
        switch ($token) {
149
            case ')':
150
                $this->processTokenClosingParenthesis();
151
                break;
152
            case '(':
153
                $this->processTokenOpeningParenthesis();
154
                break;
155
            case ConditionParser::LOGICAL_OR:
156
            case ConditionParser::LOGICAL_AND:
157
                $this->processTokenLogicalOperator($token);
158
                break;
159
            default:
160
                $this->processTokenCondition($token);
161
                break;
162
        }
163
    }
164
165
    protected function processTokenClosingParenthesis()
166
    {
167
        $this->addError('Parenthesis closes invalid group.', 1457969163);
168
    }
169
170
    protected function processTokenOpeningParenthesis()
171
    {
172
        $groupNode = $this->getGroupNode($this->state->getCurrentExpression());
173
174
        $stateSave = $this->state;
175
        $currentExpression = array_slice($this->state->getCurrentExpression(), count($groupNode) + 2);
176
        $stateSave->setCurrentExpression($currentExpression);
177
178
        $newState = $this->getNewState();
179
        $newState->setCurrentExpression($groupNode);
180
181
        $this->state = $newState;
182
183
        $node = $this->getNodeRecursive();
184
185
        $this->state = $stateSave;
186
        $this->state->setCurrentNode($node);
187
    }
188
189
    protected function processTokenLogicalOperator($operator)
190
    {
191
        if (null === $this->state->getCurrentNode()) {
192
            $this->addError('Logical operator must be preceded by a valid operation.', 1457544986);
193
        } else {
194
            if (ConditionParser::LOGICAL_OR === $operator) {
195
                if (null !== $this->state->getLastOrNode()) {
196
                    $node = $this->getNode(
197
                        BooleanNode::class,
198
                        [$this->state->getLastOrNode(), $this->state->getCurrentNode(), $operator]
199
                    );
200
                    $this->state->setCurrentNode($node);
201
                }
202
203
                $this->state->setLastOrNode($this->state->getCurrentNode());
204
            } else {
205
                $this->state
206
                    ->setCurrentLeftNode($this->state->getCurrentNode())
207
                    ->deleteCurrentNode();
208
            }
209
210
            $this->state
211
                ->setCurrentOperator($operator)
212
                ->shiftCurrentExpression();
213
        }
214
    }
215
216
    protected function processTokenCondition($condition)
217
    {
218
        if (false === $this->condition->hasItem($condition)) {
219
            $this->addError('The condition "' . $condition . '" does not exist.', 1457628378);
220
        } else {
221
            $node = $this->getNode(
222
                ConditionNode::class,
223
                [
224
                    $condition,
225
                    $this->condition->getItem($condition)
226
                ]
227
            );
228
            $this->state
229
                ->setCurrentNode($node)
230
                ->shiftCurrentExpression();
231
        }
232
    }
233
234
    /**
235
     * @param string $nodeClassName
236
     * @param array  $arguments
237
     * @return NodeInterface
238
     */
239
    protected function getNode($nodeClassName, array $arguments)
240
    {
241
        return call_user_func_array(
242
            [GeneralUtility::class, 'makeInstance'],
243
            array_merge([$nodeClassName], $arguments)
244
        );
245
    }
246
247
    /**
248
     * Will fetch for a group of operations in a given array: the first item
249
     * must be a parenthesis. If its closing parenthesis is found, then the
250
     * inner part of the group is returned. Example:
251
     *
252
     * Input: (cond1 && (cond2 || cond3)) && cond4
253
     * Output: cond1 && (cond2 || cond3)
254
     *
255
     * @param array $splitCondition
256
     * @return array
257
     */
258
    protected function getGroupNode(array $splitCondition)
259
    {
260
        $parenthesis = 1;
261
        $index = 0;
262
        while ($parenthesis > 0) {
263
            $index++;
264
            if ($index > count($splitCondition)) {
265
                $parenthesis = -1;
266
                break;
267
            }
268
269
            if ('(' === $splitCondition[$index]) {
270
                $parenthesis++;
271
            }
272
            if (')' === $splitCondition[$index]) {
273
                $parenthesis--;
274
            }
275
        }
276
277
        $finalSplitCondition = [];
278
        if (-1 === $parenthesis) {
279
            $this->addError('Parenthesis not correctly closed.', 1457544856);
280
        } else {
281
            for ($i = 1; $i < $index; $i++) {
282
                $finalSplitCondition[] = $splitCondition[$i];
283
            }
284
        }
285
286
        return $finalSplitCondition;
287
    }
288
289
    /**
290
     * Will split a condition expression string in an exploded array where each
291
     * entry represents an operation.
292
     *
293
     * @param string $condition
294
     * @return array
295
     */
296
    protected function splitConditionExpression($condition)
297
    {
298
        preg_match_all('/(\w+|\(|\)|\&\&|\|\|)/', trim($condition), $result);
299
300
        return $result[0];
301
    }
302
303
    /**
304
     * @return ConditionParserState
305
     */
306
    private function getNewState()
307
    {
308
        return new ConditionParserState;
309
    }
310
311
    /**
312
     * @param string $message
313
     * @param int    $code
314
     */
315
    private function addError($message, $code)
316
    {
317
        $error = new Error($message, $code);
318
        $this->result->addError($error);
319
    }
320
}
321