Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.64% covered (success)
92.64%
151 / 163
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Parser
92.64% covered (success)
92.64%
151 / 163
40.00% covered (danger)
40.00%
2 / 5
58.30
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 parse
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getExpression
93.06% covered (success)
93.06%
134 / 144
0.00% covered (danger)
0.00%
0 / 1
44.65
 buildComposite
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
7.23
 getNextToken
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Symftony\Xpression;
6
7use Symftony\Xpression\Exception\Lexer\LexerException;
8use Symftony\Xpression\Exception\Parser\ForbiddenTokenException;
9use Symftony\Xpression\Exception\Parser\InvalidExpressionException;
10use Symftony\Xpression\Exception\Parser\ParserException;
11use Symftony\Xpression\Exception\Parser\UnexpectedTokenException;
12use Symftony\Xpression\Exception\Parser\UnknownCompositeTypeException;
13use Symftony\Xpression\Exception\Parser\UnsupportedTokenTypeException;
14use Symftony\Xpression\Expr\ExpressionBuilderInterface;
15
16class Parser
17{
18    /**
19     * @var int Keep the lexer current index
20     */
21    public int $lexerIndex = 0;
22
23    /**
24     * @var array|int[]
25     */
26    protected array $precedence = [
27        Lexer::T_AND => 15,
28        Lexer::T_NOT_AND => 14,
29        Lexer::T_OR => 10,
30        Lexer::T_XOR => 9,
31        Lexer::T_NOT_OR => 8,
32    ];
33
34    private Lexer $lexer;
35
36    private ExpressionBuilderInterface $expressionBuilder;
37
38    /**
39     * @var int Bitwise of all allowed operator. Default was Lexer::T_ALL
40     */
41    private int $allowedTokenType;
42
43    /**
44     * @var int bitwise of ExpressionBuilder supported operator
45     */
46    private int $supportedTokenType;
47
48    public function __construct(ExpressionBuilderInterface $expressionBuilder)
49    {
50        $this->lexer = new Lexer();
51        $this->expressionBuilder = $expressionBuilder;
52    }
53
54    /**
55     * @throws InvalidExpressionException
56     */
57    public function parse(string $input, ?int $allowedTokenType = null): mixed
58    {
59        $this->allowedTokenType = null !== $allowedTokenType ? $allowedTokenType : Lexer::T_ALL;
60        $this->supportedTokenType = $this->expressionBuilder->getSupportedTokenType();
61
62        try {
63            $this->lexer->setInput($input);
64            $this->lexer->moveNext();
65
66            return $this->getExpression();
67        } catch (LexerException $exception) {
68            throw new InvalidExpressionException($input, '', 0, $exception);
69        } catch (ParserException $exception) {
70            throw new InvalidExpressionException($input, '', 0, $exception);
71        }
72    }
73
74    /**
75     * @throws ForbiddenTokenException
76     * @throws UnexpectedTokenException
77     * @throws UnsupportedTokenTypeException
78     */
79    private function getExpression(mixed $previousExpression = null): mixed
80    {
81        $expression = $previousExpression ?: null;
82        $expectedTokenType = null !== $previousExpression ? Lexer::T_COMPOSITE : Lexer::T_OPEN_PARENTHESIS | Lexer::T_INPUT_PARAMETER;
83        $expressions = [];
84        $tokenPrecedence = null;
85
86        $hasOpenParenthesis = false;
87
88        $compositeOperator = null;
89        $contains = false;
90        $containsValue = null;
91        $comparisonFirstOperande = null;
92        $comparisonMultipleOperande = false;
93        $comparisonMethod = null;
94
95        while ($currentToken = $this->getNextToken()) {
96            $currentTokenType = $currentToken['type'];
97            $currentTokenIndex = $this->lexerIndex;
98            ++$this->lexerIndex;
99
100            if (!($this->supportedTokenType & $currentTokenType)) {
101                throw new UnsupportedTokenTypeException($currentToken, $this->lexer->getTokenSyntax($this->supportedTokenType));
102            }
103
104            if (!($this->allowedTokenType & $currentTokenType)) {
105                throw new ForbiddenTokenException($currentToken, $this->lexer->getTokenSyntax($this->allowedTokenType));
106            }
107
108            if (!($expectedTokenType & $currentTokenType)) {
109                throw new UnexpectedTokenException($currentToken, $this->lexer->getTokenSyntax($expectedTokenType));
110            }
111
112            switch ($currentTokenType) {
113                case Lexer::T_OPEN_PARENTHESIS:
114                    $expression = $this->getExpression();
115                    $hasOpenParenthesis = true;
116                    $expectedTokenType = Lexer::T_CLOSE_PARENTHESIS;
117
118                    break;
119
120                case Lexer::T_CLOSE_PARENTHESIS:
121                    if (!$hasOpenParenthesis) {
122                        $this->lexerIndex = $currentTokenIndex;
123                        $this->lexer->resetPosition($currentTokenIndex);
124                        $this->lexer->moveNext();
125
126                        break 2;
127                    }
128                    $hasOpenParenthesis = false;
129                    $expectedTokenType = Lexer::T_COMPOSITE | Lexer::T_CLOSE_PARENTHESIS;
130
131                    break;
132
133                case Lexer::T_COMMA:
134                    $expectedTokenType = Lexer::T_OPERAND;
135
136                    break;
137
138                case Lexer::T_INPUT_PARAMETER:
139                    $currentTokenValue = $this->expressionBuilder->parameter($currentToken['value'], null !== $comparisonFirstOperande);
140
141                // no break
142                case Lexer::T_STRING:
143                    if (!isset($currentTokenValue)) {
144                        $currentTokenValue = $this->expressionBuilder->string($currentToken['value']);
145                    }
146
147                // no break
148                case Lexer::T_INTEGER:
149                case Lexer::T_FLOAT:
150                    if (!isset($currentTokenValue)) {
151                        $currentTokenValue = $currentToken['value'];
152                    }
153                    if (null === $comparisonFirstOperande) {
154                        $comparisonFirstOperande = $currentTokenValue;
155                        $expectedTokenType = Lexer::T_COMPARISON;
156                        $currentTokenValue = null;
157
158                        break;
159                    }
160
161                    if (\is_array($comparisonMultipleOperande)) {
162                        $comparisonMultipleOperande[] = $currentTokenValue;
163                        $expectedTokenType = Lexer::T_COMMA | Lexer::T_CLOSE_SQUARE_BRACKET;
164                        $currentTokenValue = null;
165
166                        break;
167                    }
168
169                    if ($contains) {
170                        $containsValue = $currentTokenValue;
171                        $expectedTokenType = Lexer::T_DOUBLE_CLOSE_CURLY_BRACKET;
172                        $currentTokenValue = null;
173
174                        break;
175                    }
176                    $expression = \call_user_func_array([$this->expressionBuilder, $comparisonMethod], [$comparisonFirstOperande, $currentTokenValue]);
177                    $comparisonFirstOperande = null;
178                    $comparisonMethod = null;
179                    $currentTokenValue = null;
180                    $expectedTokenType = Lexer::T_COMPOSITE | Lexer::T_CLOSE_PARENTHESIS;
181
182                    break;
183
184                case Lexer::T_EQUALS:
185                    $comparisonMethod = 'eq';
186                    $expectedTokenType = Lexer::T_OPERAND;
187
188                    break;
189
190                case Lexer::T_NOT_EQUALS:
191                    $comparisonMethod = 'neq';
192                    $expectedTokenType = Lexer::T_OPERAND;
193
194                    break;
195
196                case Lexer::T_GREATER_THAN:
197                    $comparisonMethod = 'gt';
198                    $expectedTokenType = Lexer::T_OPERAND;
199
200                    break;
201
202                case Lexer::T_GREATER_THAN_EQUALS:
203                    $comparisonMethod = 'gte';
204                    $expectedTokenType = Lexer::T_OPERAND;
205
206                    break;
207
208                case Lexer::T_LOWER_THAN:
209                    $comparisonMethod = 'lt';
210                    $expectedTokenType = Lexer::T_OPERAND;
211
212                    break;
213
214                case Lexer::T_LOWER_THAN_EQUALS:
215                    $comparisonMethod = 'lte';
216                    $expectedTokenType = Lexer::T_OPERAND;
217
218                    break;
219
220                case Lexer::T_NOT_OPEN_SQUARE_BRACKET:
221                    $comparisonMethod = 'notIn';
222                    $comparisonMultipleOperande = [];
223                    $expectedTokenType = Lexer::T_OPERAND | Lexer::T_CLOSE_SQUARE_BRACKET;
224
225                    break;
226
227                case Lexer::T_OPEN_SQUARE_BRACKET:
228                    $comparisonMethod = 'in';
229                    $comparisonMultipleOperande = [];
230                    $expectedTokenType = Lexer::T_OPERAND | Lexer::T_CLOSE_SQUARE_BRACKET;
231
232                    break;
233
234                case Lexer::T_CLOSE_SQUARE_BRACKET:
235                    $expression = \call_user_func_array([$this->expressionBuilder, $comparisonMethod], [$comparisonFirstOperande, $comparisonMultipleOperande]);
236                    $comparisonMethod = null;
237                    $comparisonFirstOperande = null;
238                    $comparisonMultipleOperande = false;
239                    $expectedTokenType = Lexer::T_COMPOSITE | Lexer::T_CLOSE_PARENTHESIS;
240
241                    break;
242
243                case Lexer::T_DOUBLE_OPEN_CURLY_BRACKET:
244                    $comparisonMethod = 'contains';
245                    $contains = true;
246                    $expectedTokenType = Lexer::T_OPERAND | Lexer::T_DOUBLE_CLOSE_CURLY_BRACKET;
247
248                    break;
249
250                case Lexer::T_NOT_DOUBLE_OPEN_CURLY_BRACKET:
251                    $comparisonMethod = 'notContains';
252                    $contains = true;
253                    $expectedTokenType = Lexer::T_OPERAND | Lexer::T_DOUBLE_CLOSE_CURLY_BRACKET;
254
255                    break;
256
257                case Lexer::T_DOUBLE_CLOSE_CURLY_BRACKET:
258                    $expression = \call_user_func_array([$this->expressionBuilder, $comparisonMethod], [$comparisonFirstOperande, $containsValue]);
259                    $comparisonMethod = null;
260                    $comparisonFirstOperande = null;
261                    $contains = false;
262                    $expectedTokenType = Lexer::T_COMPOSITE | Lexer::T_CLOSE_PARENTHESIS;
263
264                    break;
265
266                case Lexer::T_AND:
267                case Lexer::T_NOT_AND:
268                case Lexer::T_OR:
269                case Lexer::T_NOT_OR:
270                case Lexer::T_XOR:
271                    $currentTokenPrecedence = $this->precedence[$currentTokenType];
272                    if (null === $compositeOperator || $currentTokenType === $compositeOperator) {
273                        $expressions[] = $expression;
274                        $expression = null;
275                        $compositeOperator = $currentTokenType;
276                        $tokenPrecedence = $currentTokenPrecedence;
277                        $expectedTokenType = Lexer::T_OPEN_PARENTHESIS | Lexer::T_INPUT_PARAMETER;
278
279                        break;
280                    }
281
282                    if ($currentTokenPrecedence < $tokenPrecedence) {
283                        $expressions[] = $expression;
284                        $expression = null;
285                        $expressions = [$this->buildComposite($compositeOperator, $expressions)];
286                        $compositeOperator = $currentTokenType;
287                        $tokenPrecedence = $currentTokenPrecedence;
288                        $expectedTokenType = Lexer::T_OPEN_PARENTHESIS | Lexer::T_INPUT_PARAMETER;
289
290                        break;
291                    }
292
293                    if ($currentTokenPrecedence > $tokenPrecedence) {
294                        $this->lexerIndex = $currentTokenIndex;
295                        $this->lexer->resetPosition($currentTokenIndex);
296                        $this->lexer->moveNext();
297                        $expression = $this->getExpression($expression);
298
299                        break;
300                    }
301
302                    throw new \LogicException(sprintf(
303                        'Token precedence error. Current token precedence %s must be different than %s',
304                        $currentTokenPrecedence,
305                        $tokenPrecedence
306                    ));
307
308                default:
309                    throw new \LogicException(sprintf(
310                        'Token mismatch. Expected token %s given %s',
311                        $this->lexer->getLiteral($expectedTokenType),
312                        $currentTokenType
313                    ));
314            }
315        }
316
317        if (null !== $expression) {
318            $expressions[] = $expression;
319        }
320
321        if (1 === \count($expressions)) {
322            return $expressions[0];
323        }
324
325        return $this->buildComposite($compositeOperator, $expressions);
326    }
327
328    /**
329     * @param mixed $type
330     * @param mixed $expressions
331     *
332     * @return mixed
333     *
334     * @throws UnknownCompositeTypeException
335     */
336    private function buildComposite($type, $expressions)
337    {
338        switch ($type) {
339            case Lexer::T_AND:
340                return $this->expressionBuilder->andX($expressions);
341
342            case Lexer::T_NOT_AND:
343                return $this->expressionBuilder->nandX($expressions);
344
345            case Lexer::T_OR:
346                return $this->expressionBuilder->orX($expressions);
347
348            case Lexer::T_NOT_OR:
349                return $this->expressionBuilder->norX($expressions);
350
351            case Lexer::T_XOR:
352                return $this->expressionBuilder->xorX($expressions);
353
354            default:
355                throw new UnknownCompositeTypeException($type);
356        }
357    }
358
359    /**
360     * @return null|array
361     */
362    private function getNextToken(): mixed
363    {
364        $this->lexer->moveNext();
365
366        return $this->lexer->token;
367    }
368}