diff options
author | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
---|---|---|
committer | polo <ordipolo@gmx.fr> | 2024-08-13 23:45:21 +0200 |
commit | bf6655a534a6775d30cafa67bd801276bda1d98d (patch) | |
tree | c6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Query/Parser.php | |
parent | 94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff) | |
download | AppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip |
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Query/Parser.php')
-rw-r--r-- | vendor/doctrine/orm/src/Query/Parser.php | 3269 |
1 files changed, 3269 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Query/Parser.php b/vendor/doctrine/orm/src/Query/Parser.php new file mode 100644 index 0000000..e948f2c --- /dev/null +++ b/vendor/doctrine/orm/src/Query/Parser.php | |||
@@ -0,0 +1,3269 @@ | |||
1 | <?php | ||
2 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Query; | ||
6 | |||
7 | use Doctrine\Common\Lexer\Token; | ||
8 | use Doctrine\ORM\EntityManagerInterface; | ||
9 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
11 | use Doctrine\ORM\Query; | ||
12 | use Doctrine\ORM\Query\AST\Functions; | ||
13 | use LogicException; | ||
14 | use ReflectionClass; | ||
15 | |||
16 | use function array_search; | ||
17 | use function assert; | ||
18 | use function class_exists; | ||
19 | use function count; | ||
20 | use function implode; | ||
21 | use function in_array; | ||
22 | use function interface_exists; | ||
23 | use function is_string; | ||
24 | use function sprintf; | ||
25 | use function str_contains; | ||
26 | use function strlen; | ||
27 | use function strpos; | ||
28 | use function strrpos; | ||
29 | use function strtolower; | ||
30 | use function substr; | ||
31 | |||
32 | /** | ||
33 | * An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language. | ||
34 | * Parses a DQL query, reports any errors in it, and generates an AST. | ||
35 | * | ||
36 | * @psalm-type DqlToken = Token<TokenType, string> | ||
37 | * @psalm-type QueryComponent = array{ | ||
38 | * metadata?: ClassMetadata<object>, | ||
39 | * parent?: string|null, | ||
40 | * relation?: AssociationMapping|null, | ||
41 | * map?: string|null, | ||
42 | * resultVariable?: AST\Node|string, | ||
43 | * nestingLevel: int, | ||
44 | * token: DqlToken, | ||
45 | * } | ||
46 | */ | ||
47 | final class Parser | ||
48 | { | ||
49 | /** | ||
50 | * @readonly Maps BUILT-IN string function names to AST class names. | ||
51 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
52 | */ | ||
53 | private static array $stringFunctions = [ | ||
54 | 'concat' => Functions\ConcatFunction::class, | ||
55 | 'substring' => Functions\SubstringFunction::class, | ||
56 | 'trim' => Functions\TrimFunction::class, | ||
57 | 'lower' => Functions\LowerFunction::class, | ||
58 | 'upper' => Functions\UpperFunction::class, | ||
59 | 'identity' => Functions\IdentityFunction::class, | ||
60 | ]; | ||
61 | |||
62 | /** | ||
63 | * @readonly Maps BUILT-IN numeric function names to AST class names. | ||
64 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
65 | */ | ||
66 | private static array $numericFunctions = [ | ||
67 | 'length' => Functions\LengthFunction::class, | ||
68 | 'locate' => Functions\LocateFunction::class, | ||
69 | 'abs' => Functions\AbsFunction::class, | ||
70 | 'sqrt' => Functions\SqrtFunction::class, | ||
71 | 'mod' => Functions\ModFunction::class, | ||
72 | 'size' => Functions\SizeFunction::class, | ||
73 | 'date_diff' => Functions\DateDiffFunction::class, | ||
74 | 'bit_and' => Functions\BitAndFunction::class, | ||
75 | 'bit_or' => Functions\BitOrFunction::class, | ||
76 | |||
77 | // Aggregate functions | ||
78 | 'min' => Functions\MinFunction::class, | ||
79 | 'max' => Functions\MaxFunction::class, | ||
80 | 'avg' => Functions\AvgFunction::class, | ||
81 | 'sum' => Functions\SumFunction::class, | ||
82 | 'count' => Functions\CountFunction::class, | ||
83 | ]; | ||
84 | |||
85 | /** | ||
86 | * @readonly Maps BUILT-IN datetime function names to AST class names. | ||
87 | * @psalm-var array<string, class-string<Functions\FunctionNode>> | ||
88 | */ | ||
89 | private static array $datetimeFunctions = [ | ||
90 | 'current_date' => Functions\CurrentDateFunction::class, | ||
91 | 'current_time' => Functions\CurrentTimeFunction::class, | ||
92 | 'current_timestamp' => Functions\CurrentTimestampFunction::class, | ||
93 | 'date_add' => Functions\DateAddFunction::class, | ||
94 | 'date_sub' => Functions\DateSubFunction::class, | ||
95 | ]; | ||
96 | |||
97 | /* | ||
98 | * Expressions that were encountered during parsing of identifiers and expressions | ||
99 | * and still need to be validated. | ||
100 | */ | ||
101 | |||
102 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
103 | private array $deferredIdentificationVariables = []; | ||
104 | |||
105 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */ | ||
106 | private array $deferredPathExpressions = []; | ||
107 | |||
108 | /** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */ | ||
109 | private array $deferredResultVariables = []; | ||
110 | |||
111 | /** @psalm-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */ | ||
112 | private array $deferredNewObjectExpressions = []; | ||
113 | |||
114 | /** | ||
115 | * The lexer. | ||
116 | */ | ||
117 | private readonly Lexer $lexer; | ||
118 | |||
119 | /** | ||
120 | * The parser result. | ||
121 | */ | ||
122 | private readonly ParserResult $parserResult; | ||
123 | |||
124 | /** | ||
125 | * The EntityManager. | ||
126 | */ | ||
127 | private readonly EntityManagerInterface $em; | ||
128 | |||
129 | /** | ||
130 | * Map of declared query components in the parsed query. | ||
131 | * | ||
132 | * @psalm-var array<string, QueryComponent> | ||
133 | */ | ||
134 | private array $queryComponents = []; | ||
135 | |||
136 | /** | ||
137 | * Keeps the nesting level of defined ResultVariables. | ||
138 | */ | ||
139 | private int $nestingLevel = 0; | ||
140 | |||
141 | /** | ||
142 | * Any additional custom tree walkers that modify the AST. | ||
143 | * | ||
144 | * @psalm-var list<class-string<TreeWalker>> | ||
145 | */ | ||
146 | private array $customTreeWalkers = []; | ||
147 | |||
148 | /** | ||
149 | * The custom last tree walker, if any, that is responsible for producing the output. | ||
150 | * | ||
151 | * @var class-string<SqlWalker>|null | ||
152 | */ | ||
153 | private $customOutputWalker; | ||
154 | |||
155 | /** @psalm-var array<string, AST\SelectExpression> */ | ||
156 | private array $identVariableExpressions = []; | ||
157 | |||
158 | /** | ||
159 | * Creates a new query parser object. | ||
160 | * | ||
161 | * @param Query $query The Query to parse. | ||
162 | */ | ||
163 | public function __construct(private readonly Query $query) | ||
164 | { | ||
165 | $this->em = $query->getEntityManager(); | ||
166 | $this->lexer = new Lexer((string) $query->getDQL()); | ||
167 | $this->parserResult = new ParserResult(); | ||
168 | } | ||
169 | |||
170 | /** | ||
171 | * Sets a custom tree walker that produces output. | ||
172 | * This tree walker will be run last over the AST, after any other walkers. | ||
173 | * | ||
174 | * @psalm-param class-string<SqlWalker> $className | ||
175 | */ | ||
176 | public function setCustomOutputTreeWalker(string $className): void | ||
177 | { | ||
178 | $this->customOutputWalker = $className; | ||
179 | } | ||
180 | |||
181 | /** | ||
182 | * Adds a custom tree walker for modifying the AST. | ||
183 | * | ||
184 | * @psalm-param class-string<TreeWalker> $className | ||
185 | */ | ||
186 | public function addCustomTreeWalker(string $className): void | ||
187 | { | ||
188 | $this->customTreeWalkers[] = $className; | ||
189 | } | ||
190 | |||
191 | /** | ||
192 | * Gets the lexer used by the parser. | ||
193 | */ | ||
194 | public function getLexer(): Lexer | ||
195 | { | ||
196 | return $this->lexer; | ||
197 | } | ||
198 | |||
199 | /** | ||
200 | * Gets the ParserResult that is being filled with information during parsing. | ||
201 | */ | ||
202 | public function getParserResult(): ParserResult | ||
203 | { | ||
204 | return $this->parserResult; | ||
205 | } | ||
206 | |||
207 | /** | ||
208 | * Gets the EntityManager used by the parser. | ||
209 | */ | ||
210 | public function getEntityManager(): EntityManagerInterface | ||
211 | { | ||
212 | return $this->em; | ||
213 | } | ||
214 | |||
215 | /** | ||
216 | * Parses and builds AST for the given Query. | ||
217 | */ | ||
218 | public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
219 | { | ||
220 | // Parse & build AST | ||
221 | $AST = $this->QueryLanguage(); | ||
222 | |||
223 | // Process any deferred validations of some nodes in the AST. | ||
224 | // This also allows post-processing of the AST for modification purposes. | ||
225 | $this->processDeferredIdentificationVariables(); | ||
226 | |||
227 | if ($this->deferredPathExpressions) { | ||
228 | $this->processDeferredPathExpressions(); | ||
229 | } | ||
230 | |||
231 | if ($this->deferredResultVariables) { | ||
232 | $this->processDeferredResultVariables(); | ||
233 | } | ||
234 | |||
235 | if ($this->deferredNewObjectExpressions) { | ||
236 | $this->processDeferredNewObjectExpressions($AST); | ||
237 | } | ||
238 | |||
239 | $this->processRootEntityAliasSelected(); | ||
240 | |||
241 | // TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot! | ||
242 | $this->fixIdentificationVariableOrder($AST); | ||
243 | |||
244 | return $AST; | ||
245 | } | ||
246 | |||
247 | /** | ||
248 | * Attempts to match the given token with the current lookahead token. | ||
249 | * | ||
250 | * If they match, updates the lookahead token; otherwise raises a syntax | ||
251 | * error. | ||
252 | * | ||
253 | * @throws QueryException If the tokens don't match. | ||
254 | */ | ||
255 | public function match(TokenType $token): void | ||
256 | { | ||
257 | $lookaheadType = $this->lexer->lookahead->type ?? null; | ||
258 | |||
259 | // Short-circuit on first condition, usually types match | ||
260 | if ($lookaheadType === $token) { | ||
261 | $this->lexer->moveNext(); | ||
262 | |||
263 | return; | ||
264 | } | ||
265 | |||
266 | // If parameter is not identifier (1-99) must be exact match | ||
267 | if ($token->value < TokenType::T_IDENTIFIER->value) { | ||
268 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
269 | } | ||
270 | |||
271 | // If parameter is keyword (200+) must be exact match | ||
272 | if ($token->value > TokenType::T_IDENTIFIER->value) { | ||
273 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
274 | } | ||
275 | |||
276 | // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) | ||
277 | if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) { | ||
278 | $this->syntaxError($this->lexer->getLiteral($token)); | ||
279 | } | ||
280 | |||
281 | $this->lexer->moveNext(); | ||
282 | } | ||
283 | |||
284 | /** | ||
285 | * Frees this parser, enabling it to be reused. | ||
286 | * | ||
287 | * @param bool $deep Whether to clean peek and reset errors. | ||
288 | * @param int $position Position to reset. | ||
289 | */ | ||
290 | public function free(bool $deep = false, int $position = 0): void | ||
291 | { | ||
292 | // WARNING! Use this method with care. It resets the scanner! | ||
293 | $this->lexer->resetPosition($position); | ||
294 | |||
295 | // Deep = true cleans peek and also any previously defined errors | ||
296 | if ($deep) { | ||
297 | $this->lexer->resetPeek(); | ||
298 | } | ||
299 | |||
300 | $this->lexer->token = null; | ||
301 | $this->lexer->lookahead = null; | ||
302 | } | ||
303 | |||
304 | /** | ||
305 | * Parses a query string. | ||
306 | */ | ||
307 | public function parse(): ParserResult | ||
308 | { | ||
309 | $AST = $this->getAST(); | ||
310 | |||
311 | $customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); | ||
312 | if ($customWalkers !== false) { | ||
313 | $this->customTreeWalkers = $customWalkers; | ||
314 | } | ||
315 | |||
316 | $customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER); | ||
317 | if ($customOutputWalker !== false) { | ||
318 | $this->customOutputWalker = $customOutputWalker; | ||
319 | } | ||
320 | |||
321 | // Run any custom tree walkers over the AST | ||
322 | if ($this->customTreeWalkers) { | ||
323 | $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents); | ||
324 | |||
325 | foreach ($this->customTreeWalkers as $walker) { | ||
326 | $treeWalkerChain->addTreeWalker($walker); | ||
327 | } | ||
328 | |||
329 | match (true) { | ||
330 | $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST), | ||
331 | $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST), | ||
332 | $AST instanceof AST\SelectStatement => $treeWalkerChain->walkSelectStatement($AST), | ||
333 | }; | ||
334 | |||
335 | $this->queryComponents = $treeWalkerChain->getQueryComponents(); | ||
336 | } | ||
337 | |||
338 | $outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class; | ||
339 | $outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents); | ||
340 | |||
341 | // Assign an SQL executor to the parser result | ||
342 | $this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST)); | ||
343 | |||
344 | return $this->parserResult; | ||
345 | } | ||
346 | |||
347 | /** | ||
348 | * Fixes order of identification variables. | ||
349 | * | ||
350 | * They have to appear in the select clause in the same order as the | ||
351 | * declarations (from ... x join ... y join ... z ...) appear in the query | ||
352 | * as the hydration process relies on that order for proper operation. | ||
353 | */ | ||
354 | private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST): void | ||
355 | { | ||
356 | if (count($this->identVariableExpressions) <= 1) { | ||
357 | return; | ||
358 | } | ||
359 | |||
360 | assert($AST instanceof AST\SelectStatement); | ||
361 | |||
362 | foreach ($this->queryComponents as $dqlAlias => $qComp) { | ||
363 | if (! isset($this->identVariableExpressions[$dqlAlias])) { | ||
364 | continue; | ||
365 | } | ||
366 | |||
367 | $expr = $this->identVariableExpressions[$dqlAlias]; | ||
368 | $key = array_search($expr, $AST->selectClause->selectExpressions, true); | ||
369 | |||
370 | unset($AST->selectClause->selectExpressions[$key]); | ||
371 | |||
372 | $AST->selectClause->selectExpressions[] = $expr; | ||
373 | } | ||
374 | } | ||
375 | |||
376 | /** | ||
377 | * Generates a new syntax error. | ||
378 | * | ||
379 | * @param string $expected Expected string. | ||
380 | * @param DqlToken|null $token Got token. | ||
381 | * | ||
382 | * @throws QueryException | ||
383 | */ | ||
384 | public function syntaxError(string $expected = '', Token|null $token = null): never | ||
385 | { | ||
386 | if ($token === null) { | ||
387 | $token = $this->lexer->lookahead; | ||
388 | } | ||
389 | |||
390 | $tokenPos = $token->position ?? '-1'; | ||
391 | |||
392 | $message = sprintf('line 0, col %d: Error: ', $tokenPos); | ||
393 | $message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected '; | ||
394 | $message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value); | ||
395 | |||
396 | throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? '')); | ||
397 | } | ||
398 | |||
399 | /** | ||
400 | * Generates a new semantical error. | ||
401 | * | ||
402 | * @param string $message Optional message. | ||
403 | * @psalm-param DqlToken|null $token | ||
404 | * | ||
405 | * @throws QueryException | ||
406 | */ | ||
407 | public function semanticalError(string $message = '', Token|null $token = null): never | ||
408 | { | ||
409 | if ($token === null) { | ||
410 | $token = $this->lexer->lookahead ?? new Token('fake token', 42, 0); | ||
411 | } | ||
412 | |||
413 | // Minimum exposed chars ahead of token | ||
414 | $distance = 12; | ||
415 | |||
416 | // Find a position of a final word to display in error string | ||
417 | $dql = $this->query->getDQL(); | ||
418 | $length = strlen($dql); | ||
419 | $pos = $token->position + $distance; | ||
420 | $pos = strpos($dql, ' ', $length > $pos ? $pos : $length); | ||
421 | $length = $pos !== false ? $pos - $token->position : $distance; | ||
422 | |||
423 | $tokenPos = $token->position > 0 ? $token->position : '-1'; | ||
424 | $tokenStr = substr($dql, $token->position, $length); | ||
425 | |||
426 | // Building informative message | ||
427 | $message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message; | ||
428 | |||
429 | throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL())); | ||
430 | } | ||
431 | |||
432 | /** | ||
433 | * Peeks beyond the matched closing parenthesis and returns the first token after that one. | ||
434 | * | ||
435 | * @param bool $resetPeek Reset peek after finding the closing parenthesis. | ||
436 | * | ||
437 | * @psalm-return DqlToken|null | ||
438 | */ | ||
439 | private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null | ||
440 | { | ||
441 | $token = $this->lexer->peek(); | ||
442 | $numUnmatched = 1; | ||
443 | |||
444 | while ($numUnmatched > 0 && $token !== null) { | ||
445 | switch ($token->type) { | ||
446 | case TokenType::T_OPEN_PARENTHESIS: | ||
447 | ++$numUnmatched; | ||
448 | break; | ||
449 | |||
450 | case TokenType::T_CLOSE_PARENTHESIS: | ||
451 | --$numUnmatched; | ||
452 | break; | ||
453 | |||
454 | default: | ||
455 | // Do nothing | ||
456 | } | ||
457 | |||
458 | $token = $this->lexer->peek(); | ||
459 | } | ||
460 | |||
461 | if ($resetPeek) { | ||
462 | $this->lexer->resetPeek(); | ||
463 | } | ||
464 | |||
465 | return $token; | ||
466 | } | ||
467 | |||
468 | /** | ||
469 | * Checks if the given token indicates a mathematical operator. | ||
470 | * | ||
471 | * @psalm-param DqlToken|null $token | ||
472 | */ | ||
473 | private function isMathOperator(Token|null $token): bool | ||
474 | { | ||
475 | return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true); | ||
476 | } | ||
477 | |||
478 | /** | ||
479 | * Checks if the next-next (after lookahead) token starts a function. | ||
480 | * | ||
481 | * @return bool TRUE if the next-next tokens start a function, FALSE otherwise. | ||
482 | */ | ||
483 | private function isFunction(): bool | ||
484 | { | ||
485 | assert($this->lexer->lookahead !== null); | ||
486 | $lookaheadType = $this->lexer->lookahead->type; | ||
487 | $peek = $this->lexer->peek(); | ||
488 | |||
489 | $this->lexer->resetPeek(); | ||
490 | |||
491 | return $lookaheadType->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS; | ||
492 | } | ||
493 | |||
494 | /** | ||
495 | * Checks whether the given token type indicates an aggregate function. | ||
496 | * | ||
497 | * @return bool TRUE if the token type is an aggregate function, FALSE otherwise. | ||
498 | */ | ||
499 | private function isAggregateFunction(TokenType $tokenType): bool | ||
500 | { | ||
501 | return in_array( | ||
502 | $tokenType, | ||
503 | [TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT], | ||
504 | true, | ||
505 | ); | ||
506 | } | ||
507 | |||
508 | /** | ||
509 | * Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME. | ||
510 | */ | ||
511 | private function isNextAllAnySome(): bool | ||
512 | { | ||
513 | assert($this->lexer->lookahead !== null); | ||
514 | |||
515 | return in_array( | ||
516 | $this->lexer->lookahead->type, | ||
517 | [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], | ||
518 | true, | ||
519 | ); | ||
520 | } | ||
521 | |||
522 | /** | ||
523 | * Validates that the given <tt>IdentificationVariable</tt> is semantically correct. | ||
524 | * It must exist in query components list. | ||
525 | */ | ||
526 | private function processDeferredIdentificationVariables(): void | ||
527 | { | ||
528 | foreach ($this->deferredIdentificationVariables as $deferredItem) { | ||
529 | $identVariable = $deferredItem['expression']; | ||
530 | |||
531 | // Check if IdentificationVariable exists in queryComponents | ||
532 | if (! isset($this->queryComponents[$identVariable])) { | ||
533 | $this->semanticalError( | ||
534 | sprintf("'%s' is not defined.", $identVariable), | ||
535 | $deferredItem['token'], | ||
536 | ); | ||
537 | } | ||
538 | |||
539 | $qComp = $this->queryComponents[$identVariable]; | ||
540 | |||
541 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
542 | if (! isset($qComp['metadata'])) { | ||
543 | $this->semanticalError( | ||
544 | sprintf("'%s' does not point to a Class.", $identVariable), | ||
545 | $deferredItem['token'], | ||
546 | ); | ||
547 | } | ||
548 | |||
549 | // Validate if identification variable nesting level is lower or equal than the current one | ||
550 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
551 | $this->semanticalError( | ||
552 | sprintf("'%s' is used outside the scope of its declaration.", $identVariable), | ||
553 | $deferredItem['token'], | ||
554 | ); | ||
555 | } | ||
556 | } | ||
557 | } | ||
558 | |||
559 | /** | ||
560 | * Validates that the given <tt>NewObjectExpression</tt>. | ||
561 | */ | ||
562 | private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void | ||
563 | { | ||
564 | foreach ($this->deferredNewObjectExpressions as $deferredItem) { | ||
565 | $expression = $deferredItem['expression']; | ||
566 | $token = $deferredItem['token']; | ||
567 | $className = $expression->className; | ||
568 | $args = $expression->args; | ||
569 | $fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null; | ||
570 | |||
571 | // If the namespace is not given then assumes the first FROM entity namespace | ||
572 | if (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) { | ||
573 | $namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\')); | ||
574 | $fqcn = $namespace . '\\' . $className; | ||
575 | |||
576 | if (class_exists($fqcn)) { | ||
577 | $expression->className = $fqcn; | ||
578 | $className = $fqcn; | ||
579 | } | ||
580 | } | ||
581 | |||
582 | if (! class_exists($className)) { | ||
583 | $this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token); | ||
584 | } | ||
585 | |||
586 | $class = new ReflectionClass($className); | ||
587 | |||
588 | if (! $class->isInstantiable()) { | ||
589 | $this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token); | ||
590 | } | ||
591 | |||
592 | if ($class->getConstructor() === null) { | ||
593 | $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token); | ||
594 | } | ||
595 | |||
596 | if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) { | ||
597 | $this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token); | ||
598 | } | ||
599 | } | ||
600 | } | ||
601 | |||
602 | /** | ||
603 | * Validates that the given <tt>ResultVariable</tt> is semantically correct. | ||
604 | * It must exist in query components list. | ||
605 | */ | ||
606 | private function processDeferredResultVariables(): void | ||
607 | { | ||
608 | foreach ($this->deferredResultVariables as $deferredItem) { | ||
609 | $resultVariable = $deferredItem['expression']; | ||
610 | |||
611 | // Check if ResultVariable exists in queryComponents | ||
612 | if (! isset($this->queryComponents[$resultVariable])) { | ||
613 | $this->semanticalError( | ||
614 | sprintf("'%s' is not defined.", $resultVariable), | ||
615 | $deferredItem['token'], | ||
616 | ); | ||
617 | } | ||
618 | |||
619 | $qComp = $this->queryComponents[$resultVariable]; | ||
620 | |||
621 | // Check if queryComponent points to an AbstractSchemaName or a ResultVariable | ||
622 | if (! isset($qComp['resultVariable'])) { | ||
623 | $this->semanticalError( | ||
624 | sprintf("'%s' does not point to a ResultVariable.", $resultVariable), | ||
625 | $deferredItem['token'], | ||
626 | ); | ||
627 | } | ||
628 | |||
629 | // Validate if identification variable nesting level is lower or equal than the current one | ||
630 | if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { | ||
631 | $this->semanticalError( | ||
632 | sprintf("'%s' is used outside the scope of its declaration.", $resultVariable), | ||
633 | $deferredItem['token'], | ||
634 | ); | ||
635 | } | ||
636 | } | ||
637 | } | ||
638 | |||
639 | /** | ||
640 | * Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules: | ||
641 | * | ||
642 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
643 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
644 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
645 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
646 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
647 | */ | ||
648 | private function processDeferredPathExpressions(): void | ||
649 | { | ||
650 | foreach ($this->deferredPathExpressions as $deferredItem) { | ||
651 | $pathExpression = $deferredItem['expression']; | ||
652 | |||
653 | $class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable); | ||
654 | |||
655 | $field = $pathExpression->field; | ||
656 | if ($field === null) { | ||
657 | $field = $pathExpression->field = $class->identifier[0]; | ||
658 | } | ||
659 | |||
660 | // Check if field or association exists | ||
661 | if (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) { | ||
662 | $this->semanticalError( | ||
663 | 'Class ' . $class->name . ' has no field or association named ' . $field, | ||
664 | $deferredItem['token'], | ||
665 | ); | ||
666 | } | ||
667 | |||
668 | $fieldType = AST\PathExpression::TYPE_STATE_FIELD; | ||
669 | |||
670 | if (isset($class->associationMappings[$field])) { | ||
671 | $assoc = $class->associationMappings[$field]; | ||
672 | |||
673 | $fieldType = $assoc->isToOne() | ||
674 | ? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | ||
675 | : AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; | ||
676 | } | ||
677 | |||
678 | // Validate if PathExpression is one of the expected types | ||
679 | $expectedType = $pathExpression->expectedType; | ||
680 | |||
681 | if (! ($expectedType & $fieldType)) { | ||
682 | // We need to recognize which was expected type(s) | ||
683 | $expectedStringTypes = []; | ||
684 | |||
685 | // Validate state field type | ||
686 | if ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) { | ||
687 | $expectedStringTypes[] = 'StateFieldPathExpression'; | ||
688 | } | ||
689 | |||
690 | // Validate single valued association (*-to-one) | ||
691 | if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { | ||
692 | $expectedStringTypes[] = 'SingleValuedAssociationField'; | ||
693 | } | ||
694 | |||
695 | // Validate single valued association (*-to-many) | ||
696 | if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { | ||
697 | $expectedStringTypes[] = 'CollectionValuedAssociationField'; | ||
698 | } | ||
699 | |||
700 | // Build the error message | ||
701 | $semanticalError = 'Invalid PathExpression. '; | ||
702 | $semanticalError .= count($expectedStringTypes) === 1 | ||
703 | ? 'Must be a ' . $expectedStringTypes[0] . '.' | ||
704 | : implode(' or ', $expectedStringTypes) . ' expected.'; | ||
705 | |||
706 | $this->semanticalError($semanticalError, $deferredItem['token']); | ||
707 | } | ||
708 | |||
709 | // We need to force the type in PathExpression | ||
710 | $pathExpression->type = $fieldType; | ||
711 | } | ||
712 | } | ||
713 | |||
714 | private function processRootEntityAliasSelected(): void | ||
715 | { | ||
716 | if (! count($this->identVariableExpressions)) { | ||
717 | return; | ||
718 | } | ||
719 | |||
720 | foreach ($this->identVariableExpressions as $dqlAlias => $expr) { | ||
721 | if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) { | ||
722 | return; | ||
723 | } | ||
724 | } | ||
725 | |||
726 | $this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.'); | ||
727 | } | ||
728 | |||
729 | /** | ||
730 | * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement | ||
731 | */ | ||
732 | public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement | ||
733 | { | ||
734 | $statement = null; | ||
735 | |||
736 | $this->lexer->moveNext(); | ||
737 | |||
738 | switch ($this->lexer->lookahead->type ?? null) { | ||
739 | case TokenType::T_SELECT: | ||
740 | $statement = $this->SelectStatement(); | ||
741 | break; | ||
742 | |||
743 | case TokenType::T_UPDATE: | ||
744 | $statement = $this->UpdateStatement(); | ||
745 | break; | ||
746 | |||
747 | case TokenType::T_DELETE: | ||
748 | $statement = $this->DeleteStatement(); | ||
749 | break; | ||
750 | |||
751 | default: | ||
752 | $this->syntaxError('SELECT, UPDATE or DELETE'); | ||
753 | break; | ||
754 | } | ||
755 | |||
756 | // Check for end of string | ||
757 | if ($this->lexer->lookahead !== null) { | ||
758 | $this->syntaxError('end of string'); | ||
759 | } | ||
760 | |||
761 | return $statement; | ||
762 | } | ||
763 | |||
764 | /** | ||
765 | * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
766 | */ | ||
767 | public function SelectStatement(): AST\SelectStatement | ||
768 | { | ||
769 | $selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause()); | ||
770 | |||
771 | $selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
772 | $selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
773 | $selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
774 | $selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
775 | |||
776 | return $selectStatement; | ||
777 | } | ||
778 | |||
779 | /** | ||
780 | * UpdateStatement ::= UpdateClause [WhereClause] | ||
781 | */ | ||
782 | public function UpdateStatement(): AST\UpdateStatement | ||
783 | { | ||
784 | $updateStatement = new AST\UpdateStatement($this->UpdateClause()); | ||
785 | |||
786 | $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
787 | |||
788 | return $updateStatement; | ||
789 | } | ||
790 | |||
791 | /** | ||
792 | * DeleteStatement ::= DeleteClause [WhereClause] | ||
793 | */ | ||
794 | public function DeleteStatement(): AST\DeleteStatement | ||
795 | { | ||
796 | $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); | ||
797 | |||
798 | $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
799 | |||
800 | return $deleteStatement; | ||
801 | } | ||
802 | |||
803 | /** | ||
804 | * IdentificationVariable ::= identifier | ||
805 | */ | ||
806 | public function IdentificationVariable(): string | ||
807 | { | ||
808 | $this->match(TokenType::T_IDENTIFIER); | ||
809 | |||
810 | assert($this->lexer->token !== null); | ||
811 | $identVariable = $this->lexer->token->value; | ||
812 | |||
813 | $this->deferredIdentificationVariables[] = [ | ||
814 | 'expression' => $identVariable, | ||
815 | 'nestingLevel' => $this->nestingLevel, | ||
816 | 'token' => $this->lexer->token, | ||
817 | ]; | ||
818 | |||
819 | return $identVariable; | ||
820 | } | ||
821 | |||
822 | /** | ||
823 | * AliasIdentificationVariable = identifier | ||
824 | */ | ||
825 | public function AliasIdentificationVariable(): string | ||
826 | { | ||
827 | $this->match(TokenType::T_IDENTIFIER); | ||
828 | |||
829 | assert($this->lexer->token !== null); | ||
830 | $aliasIdentVariable = $this->lexer->token->value; | ||
831 | $exists = isset($this->queryComponents[$aliasIdentVariable]); | ||
832 | |||
833 | if ($exists) { | ||
834 | $this->semanticalError( | ||
835 | sprintf("'%s' is already defined.", $aliasIdentVariable), | ||
836 | $this->lexer->token, | ||
837 | ); | ||
838 | } | ||
839 | |||
840 | return $aliasIdentVariable; | ||
841 | } | ||
842 | |||
843 | /** | ||
844 | * AbstractSchemaName ::= fully_qualified_name | identifier | ||
845 | */ | ||
846 | public function AbstractSchemaName(): string | ||
847 | { | ||
848 | if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) { | ||
849 | $this->match(TokenType::T_FULLY_QUALIFIED_NAME); | ||
850 | assert($this->lexer->token !== null); | ||
851 | |||
852 | return $this->lexer->token->value; | ||
853 | } | ||
854 | |||
855 | $this->match(TokenType::T_IDENTIFIER); | ||
856 | assert($this->lexer->token !== null); | ||
857 | |||
858 | return $this->lexer->token->value; | ||
859 | } | ||
860 | |||
861 | /** | ||
862 | * Validates an AbstractSchemaName, making sure the class exists. | ||
863 | * | ||
864 | * @param string $schemaName The name to validate. | ||
865 | * | ||
866 | * @throws QueryException if the name does not exist. | ||
867 | */ | ||
868 | private function validateAbstractSchemaName(string $schemaName): void | ||
869 | { | ||
870 | assert($this->lexer->token !== null); | ||
871 | if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) { | ||
872 | $this->semanticalError( | ||
873 | sprintf("Class '%s' is not defined.", $schemaName), | ||
874 | $this->lexer->token, | ||
875 | ); | ||
876 | } | ||
877 | } | ||
878 | |||
879 | /** | ||
880 | * AliasResultVariable ::= identifier | ||
881 | */ | ||
882 | public function AliasResultVariable(): string | ||
883 | { | ||
884 | $this->match(TokenType::T_IDENTIFIER); | ||
885 | |||
886 | assert($this->lexer->token !== null); | ||
887 | $resultVariable = $this->lexer->token->value; | ||
888 | $exists = isset($this->queryComponents[$resultVariable]); | ||
889 | |||
890 | if ($exists) { | ||
891 | $this->semanticalError( | ||
892 | sprintf("'%s' is already defined.", $resultVariable), | ||
893 | $this->lexer->token, | ||
894 | ); | ||
895 | } | ||
896 | |||
897 | return $resultVariable; | ||
898 | } | ||
899 | |||
900 | /** | ||
901 | * ResultVariable ::= identifier | ||
902 | */ | ||
903 | public function ResultVariable(): string | ||
904 | { | ||
905 | $this->match(TokenType::T_IDENTIFIER); | ||
906 | |||
907 | assert($this->lexer->token !== null); | ||
908 | $resultVariable = $this->lexer->token->value; | ||
909 | |||
910 | // Defer ResultVariable validation | ||
911 | $this->deferredResultVariables[] = [ | ||
912 | 'expression' => $resultVariable, | ||
913 | 'nestingLevel' => $this->nestingLevel, | ||
914 | 'token' => $this->lexer->token, | ||
915 | ]; | ||
916 | |||
917 | return $resultVariable; | ||
918 | } | ||
919 | |||
920 | /** | ||
921 | * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) | ||
922 | */ | ||
923 | public function JoinAssociationPathExpression(): AST\JoinAssociationPathExpression | ||
924 | { | ||
925 | $identVariable = $this->IdentificationVariable(); | ||
926 | |||
927 | if (! isset($this->queryComponents[$identVariable])) { | ||
928 | $this->semanticalError( | ||
929 | 'Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.', | ||
930 | ); | ||
931 | } | ||
932 | |||
933 | $this->match(TokenType::T_DOT); | ||
934 | $this->match(TokenType::T_IDENTIFIER); | ||
935 | |||
936 | assert($this->lexer->token !== null); | ||
937 | $field = $this->lexer->token->value; | ||
938 | |||
939 | // Validate association field | ||
940 | $class = $this->getMetadataForDqlAlias($identVariable); | ||
941 | |||
942 | if (! $class->hasAssociation($field)) { | ||
943 | $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); | ||
944 | } | ||
945 | |||
946 | return new AST\JoinAssociationPathExpression($identVariable, $field); | ||
947 | } | ||
948 | |||
949 | /** | ||
950 | * Parses an arbitrary path expression and defers semantical validation | ||
951 | * based on expected types. | ||
952 | * | ||
953 | * PathExpression ::= IdentificationVariable {"." identifier}* | ||
954 | * | ||
955 | * @psalm-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes | ||
956 | */ | ||
957 | public function PathExpression(int $expectedTypes): AST\PathExpression | ||
958 | { | ||
959 | $identVariable = $this->IdentificationVariable(); | ||
960 | $field = null; | ||
961 | |||
962 | assert($this->lexer->token !== null); | ||
963 | if ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
964 | $this->match(TokenType::T_DOT); | ||
965 | $this->match(TokenType::T_IDENTIFIER); | ||
966 | |||
967 | $field = $this->lexer->token->value; | ||
968 | |||
969 | while ($this->lexer->isNextToken(TokenType::T_DOT)) { | ||
970 | $this->match(TokenType::T_DOT); | ||
971 | $this->match(TokenType::T_IDENTIFIER); | ||
972 | $field .= '.' . $this->lexer->token->value; | ||
973 | } | ||
974 | } | ||
975 | |||
976 | // Creating AST node | ||
977 | $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); | ||
978 | |||
979 | // Defer PathExpression validation if requested to be deferred | ||
980 | $this->deferredPathExpressions[] = [ | ||
981 | 'expression' => $pathExpr, | ||
982 | 'nestingLevel' => $this->nestingLevel, | ||
983 | 'token' => $this->lexer->token, | ||
984 | ]; | ||
985 | |||
986 | return $pathExpr; | ||
987 | } | ||
988 | |||
989 | /** | ||
990 | * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression | ||
991 | */ | ||
992 | public function AssociationPathExpression(): AST\PathExpression | ||
993 | { | ||
994 | return $this->PathExpression( | ||
995 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | | ||
996 | AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION, | ||
997 | ); | ||
998 | } | ||
999 | |||
1000 | /** | ||
1001 | * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression | ||
1002 | */ | ||
1003 | public function SingleValuedPathExpression(): AST\PathExpression | ||
1004 | { | ||
1005 | return $this->PathExpression( | ||
1006 | AST\PathExpression::TYPE_STATE_FIELD | | ||
1007 | AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, | ||
1008 | ); | ||
1009 | } | ||
1010 | |||
1011 | /** | ||
1012 | * StateFieldPathExpression ::= IdentificationVariable "." StateField | ||
1013 | */ | ||
1014 | public function StateFieldPathExpression(): AST\PathExpression | ||
1015 | { | ||
1016 | return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); | ||
1017 | } | ||
1018 | |||
1019 | /** | ||
1020 | * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField | ||
1021 | */ | ||
1022 | public function SingleValuedAssociationPathExpression(): AST\PathExpression | ||
1023 | { | ||
1024 | return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); | ||
1025 | } | ||
1026 | |||
1027 | /** | ||
1028 | * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField | ||
1029 | */ | ||
1030 | public function CollectionValuedPathExpression(): AST\PathExpression | ||
1031 | { | ||
1032 | return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); | ||
1033 | } | ||
1034 | |||
1035 | /** | ||
1036 | * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} | ||
1037 | */ | ||
1038 | public function SelectClause(): AST\SelectClause | ||
1039 | { | ||
1040 | $isDistinct = false; | ||
1041 | $this->match(TokenType::T_SELECT); | ||
1042 | |||
1043 | // Check for DISTINCT | ||
1044 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
1045 | $this->match(TokenType::T_DISTINCT); | ||
1046 | |||
1047 | $isDistinct = true; | ||
1048 | } | ||
1049 | |||
1050 | // Process SelectExpressions (1..N) | ||
1051 | $selectExpressions = []; | ||
1052 | $selectExpressions[] = $this->SelectExpression(); | ||
1053 | |||
1054 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1055 | $this->match(TokenType::T_COMMA); | ||
1056 | |||
1057 | $selectExpressions[] = $this->SelectExpression(); | ||
1058 | } | ||
1059 | |||
1060 | return new AST\SelectClause($selectExpressions, $isDistinct); | ||
1061 | } | ||
1062 | |||
1063 | /** | ||
1064 | * SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression | ||
1065 | */ | ||
1066 | public function SimpleSelectClause(): AST\SimpleSelectClause | ||
1067 | { | ||
1068 | $isDistinct = false; | ||
1069 | $this->match(TokenType::T_SELECT); | ||
1070 | |||
1071 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
1072 | $this->match(TokenType::T_DISTINCT); | ||
1073 | |||
1074 | $isDistinct = true; | ||
1075 | } | ||
1076 | |||
1077 | return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct); | ||
1078 | } | ||
1079 | |||
1080 | /** | ||
1081 | * UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}* | ||
1082 | */ | ||
1083 | public function UpdateClause(): AST\UpdateClause | ||
1084 | { | ||
1085 | $this->match(TokenType::T_UPDATE); | ||
1086 | assert($this->lexer->lookahead !== null); | ||
1087 | |||
1088 | $token = $this->lexer->lookahead; | ||
1089 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1090 | |||
1091 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1092 | |||
1093 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1094 | $this->match(TokenType::T_AS); | ||
1095 | } | ||
1096 | |||
1097 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1098 | |||
1099 | $class = $this->em->getClassMetadata($abstractSchemaName); | ||
1100 | |||
1101 | // Building queryComponent | ||
1102 | $queryComponent = [ | ||
1103 | 'metadata' => $class, | ||
1104 | 'parent' => null, | ||
1105 | 'relation' => null, | ||
1106 | 'map' => null, | ||
1107 | 'nestingLevel' => $this->nestingLevel, | ||
1108 | 'token' => $token, | ||
1109 | ]; | ||
1110 | |||
1111 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1112 | |||
1113 | $this->match(TokenType::T_SET); | ||
1114 | |||
1115 | $updateItems = []; | ||
1116 | $updateItems[] = $this->UpdateItem(); | ||
1117 | |||
1118 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1119 | $this->match(TokenType::T_COMMA); | ||
1120 | |||
1121 | $updateItems[] = $this->UpdateItem(); | ||
1122 | } | ||
1123 | |||
1124 | $updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems); | ||
1125 | $updateClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
1126 | |||
1127 | return $updateClause; | ||
1128 | } | ||
1129 | |||
1130 | /** | ||
1131 | * DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
1132 | */ | ||
1133 | public function DeleteClause(): AST\DeleteClause | ||
1134 | { | ||
1135 | $this->match(TokenType::T_DELETE); | ||
1136 | |||
1137 | if ($this->lexer->isNextToken(TokenType::T_FROM)) { | ||
1138 | $this->match(TokenType::T_FROM); | ||
1139 | } | ||
1140 | |||
1141 | assert($this->lexer->lookahead !== null); | ||
1142 | $token = $this->lexer->lookahead; | ||
1143 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1144 | |||
1145 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1146 | |||
1147 | $deleteClause = new AST\DeleteClause($abstractSchemaName); | ||
1148 | |||
1149 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1150 | $this->match(TokenType::T_AS); | ||
1151 | } | ||
1152 | |||
1153 | $aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER) | ||
1154 | ? $this->AliasIdentificationVariable() | ||
1155 | : 'alias_should_have_been_set'; | ||
1156 | |||
1157 | $deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable; | ||
1158 | $class = $this->em->getClassMetadata($deleteClause->abstractSchemaName); | ||
1159 | |||
1160 | // Building queryComponent | ||
1161 | $queryComponent = [ | ||
1162 | 'metadata' => $class, | ||
1163 | 'parent' => null, | ||
1164 | 'relation' => null, | ||
1165 | 'map' => null, | ||
1166 | 'nestingLevel' => $this->nestingLevel, | ||
1167 | 'token' => $token, | ||
1168 | ]; | ||
1169 | |||
1170 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1171 | |||
1172 | return $deleteClause; | ||
1173 | } | ||
1174 | |||
1175 | /** | ||
1176 | * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}* | ||
1177 | */ | ||
1178 | public function FromClause(): AST\FromClause | ||
1179 | { | ||
1180 | $this->match(TokenType::T_FROM); | ||
1181 | |||
1182 | $identificationVariableDeclarations = []; | ||
1183 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
1184 | |||
1185 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1186 | $this->match(TokenType::T_COMMA); | ||
1187 | |||
1188 | $identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration(); | ||
1189 | } | ||
1190 | |||
1191 | return new AST\FromClause($identificationVariableDeclarations); | ||
1192 | } | ||
1193 | |||
1194 | /** | ||
1195 | * SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}* | ||
1196 | */ | ||
1197 | public function SubselectFromClause(): AST\SubselectFromClause | ||
1198 | { | ||
1199 | $this->match(TokenType::T_FROM); | ||
1200 | |||
1201 | $identificationVariables = []; | ||
1202 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
1203 | |||
1204 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1205 | $this->match(TokenType::T_COMMA); | ||
1206 | |||
1207 | $identificationVariables[] = $this->SubselectIdentificationVariableDeclaration(); | ||
1208 | } | ||
1209 | |||
1210 | return new AST\SubselectFromClause($identificationVariables); | ||
1211 | } | ||
1212 | |||
1213 | /** | ||
1214 | * WhereClause ::= "WHERE" ConditionalExpression | ||
1215 | */ | ||
1216 | public function WhereClause(): AST\WhereClause | ||
1217 | { | ||
1218 | $this->match(TokenType::T_WHERE); | ||
1219 | |||
1220 | return new AST\WhereClause($this->ConditionalExpression()); | ||
1221 | } | ||
1222 | |||
1223 | /** | ||
1224 | * HavingClause ::= "HAVING" ConditionalExpression | ||
1225 | */ | ||
1226 | public function HavingClause(): AST\HavingClause | ||
1227 | { | ||
1228 | $this->match(TokenType::T_HAVING); | ||
1229 | |||
1230 | return new AST\HavingClause($this->ConditionalExpression()); | ||
1231 | } | ||
1232 | |||
1233 | /** | ||
1234 | * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* | ||
1235 | */ | ||
1236 | public function GroupByClause(): AST\GroupByClause | ||
1237 | { | ||
1238 | $this->match(TokenType::T_GROUP); | ||
1239 | $this->match(TokenType::T_BY); | ||
1240 | |||
1241 | $groupByItems = [$this->GroupByItem()]; | ||
1242 | |||
1243 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1244 | $this->match(TokenType::T_COMMA); | ||
1245 | |||
1246 | $groupByItems[] = $this->GroupByItem(); | ||
1247 | } | ||
1248 | |||
1249 | return new AST\GroupByClause($groupByItems); | ||
1250 | } | ||
1251 | |||
1252 | /** | ||
1253 | * OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}* | ||
1254 | */ | ||
1255 | public function OrderByClause(): AST\OrderByClause | ||
1256 | { | ||
1257 | $this->match(TokenType::T_ORDER); | ||
1258 | $this->match(TokenType::T_BY); | ||
1259 | |||
1260 | $orderByItems = []; | ||
1261 | $orderByItems[] = $this->OrderByItem(); | ||
1262 | |||
1263 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1264 | $this->match(TokenType::T_COMMA); | ||
1265 | |||
1266 | $orderByItems[] = $this->OrderByItem(); | ||
1267 | } | ||
1268 | |||
1269 | return new AST\OrderByClause($orderByItems); | ||
1270 | } | ||
1271 | |||
1272 | /** | ||
1273 | * Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] | ||
1274 | */ | ||
1275 | public function Subselect(): AST\Subselect | ||
1276 | { | ||
1277 | // Increase query nesting level | ||
1278 | $this->nestingLevel++; | ||
1279 | |||
1280 | $subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause()); | ||
1281 | |||
1282 | $subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; | ||
1283 | $subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null; | ||
1284 | $subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null; | ||
1285 | $subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null; | ||
1286 | |||
1287 | // Decrease query nesting level | ||
1288 | $this->nestingLevel--; | ||
1289 | |||
1290 | return $subselect; | ||
1291 | } | ||
1292 | |||
1293 | /** | ||
1294 | * UpdateItem ::= SingleValuedPathExpression "=" NewValue | ||
1295 | */ | ||
1296 | public function UpdateItem(): AST\UpdateItem | ||
1297 | { | ||
1298 | $pathExpr = $this->SingleValuedPathExpression(); | ||
1299 | |||
1300 | $this->match(TokenType::T_EQUALS); | ||
1301 | |||
1302 | return new AST\UpdateItem($pathExpr, $this->NewValue()); | ||
1303 | } | ||
1304 | |||
1305 | /** | ||
1306 | * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression | ||
1307 | */ | ||
1308 | public function GroupByItem(): string|AST\PathExpression | ||
1309 | { | ||
1310 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
1311 | $glimpse = $this->lexer->glimpse(); | ||
1312 | |||
1313 | if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) { | ||
1314 | return $this->SingleValuedPathExpression(); | ||
1315 | } | ||
1316 | |||
1317 | assert($this->lexer->lookahead !== null); | ||
1318 | // Still need to decide between IdentificationVariable or ResultVariable | ||
1319 | $lookaheadValue = $this->lexer->lookahead->value; | ||
1320 | |||
1321 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
1322 | $this->semanticalError('Cannot group by undefined identification or result variable.'); | ||
1323 | } | ||
1324 | |||
1325 | return isset($this->queryComponents[$lookaheadValue]['metadata']) | ||
1326 | ? $this->IdentificationVariable() | ||
1327 | : $this->ResultVariable(); | ||
1328 | } | ||
1329 | |||
1330 | /** | ||
1331 | * OrderByItem ::= ( | ||
1332 | * SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression | | ||
1333 | * ScalarExpression | ResultVariable | FunctionDeclaration | ||
1334 | * ) ["ASC" | "DESC"] | ||
1335 | */ | ||
1336 | public function OrderByItem(): AST\OrderByItem | ||
1337 | { | ||
1338 | $this->lexer->peek(); // lookahead => '.' | ||
1339 | $this->lexer->peek(); // lookahead => token after '.' | ||
1340 | |||
1341 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
1342 | |||
1343 | $this->lexer->resetPeek(); | ||
1344 | |||
1345 | $glimpse = $this->lexer->glimpse(); | ||
1346 | |||
1347 | assert($this->lexer->lookahead !== null); | ||
1348 | $expr = match (true) { | ||
1349 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
1350 | $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(), | ||
1351 | $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
1352 | $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(), | ||
1353 | $this->isFunction() => $this->FunctionDeclaration(), | ||
1354 | default => $this->ResultVariable(), | ||
1355 | }; | ||
1356 | |||
1357 | $type = 'ASC'; | ||
1358 | $item = new AST\OrderByItem($expr); | ||
1359 | |||
1360 | switch (true) { | ||
1361 | case $this->lexer->isNextToken(TokenType::T_DESC): | ||
1362 | $this->match(TokenType::T_DESC); | ||
1363 | $type = 'DESC'; | ||
1364 | break; | ||
1365 | |||
1366 | case $this->lexer->isNextToken(TokenType::T_ASC): | ||
1367 | $this->match(TokenType::T_ASC); | ||
1368 | break; | ||
1369 | |||
1370 | default: | ||
1371 | // Do nothing | ||
1372 | } | ||
1373 | |||
1374 | $item->type = $type; | ||
1375 | |||
1376 | return $item; | ||
1377 | } | ||
1378 | |||
1379 | /** | ||
1380 | * NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary | | ||
1381 | * EnumPrimary | SimpleEntityExpression | "NULL" | ||
1382 | * | ||
1383 | * NOTE: Since it is not possible to correctly recognize individual types, here is the full | ||
1384 | * grammar that needs to be supported: | ||
1385 | * | ||
1386 | * NewValue ::= SimpleArithmeticExpression | "NULL" | ||
1387 | * | ||
1388 | * SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression | ||
1389 | */ | ||
1390 | public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null | ||
1391 | { | ||
1392 | if ($this->lexer->isNextToken(TokenType::T_NULL)) { | ||
1393 | $this->match(TokenType::T_NULL); | ||
1394 | |||
1395 | return null; | ||
1396 | } | ||
1397 | |||
1398 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
1399 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
1400 | assert($this->lexer->token !== null); | ||
1401 | |||
1402 | return new AST\InputParameter($this->lexer->token->value); | ||
1403 | } | ||
1404 | |||
1405 | return $this->ArithmeticExpression(); | ||
1406 | } | ||
1407 | |||
1408 | /** | ||
1409 | * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* | ||
1410 | */ | ||
1411 | public function IdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
1412 | { | ||
1413 | $joins = []; | ||
1414 | $rangeVariableDeclaration = $this->RangeVariableDeclaration(); | ||
1415 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) | ||
1416 | ? $this->IndexBy() | ||
1417 | : null; | ||
1418 | |||
1419 | $rangeVariableDeclaration->isRoot = true; | ||
1420 | |||
1421 | while ( | ||
1422 | $this->lexer->isNextToken(TokenType::T_LEFT) || | ||
1423 | $this->lexer->isNextToken(TokenType::T_INNER) || | ||
1424 | $this->lexer->isNextToken(TokenType::T_JOIN) | ||
1425 | ) { | ||
1426 | $joins[] = $this->Join(); | ||
1427 | } | ||
1428 | |||
1429 | return new AST\IdentificationVariableDeclaration( | ||
1430 | $rangeVariableDeclaration, | ||
1431 | $indexBy, | ||
1432 | $joins, | ||
1433 | ); | ||
1434 | } | ||
1435 | |||
1436 | /** | ||
1437 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | ||
1438 | * | ||
1439 | * {Internal note: WARNING: Solution is harder than a bare implementation. | ||
1440 | * Desired EBNF support: | ||
1441 | * | ||
1442 | * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) | ||
1443 | * | ||
1444 | * It demands that entire SQL generation to become programmatical. This is | ||
1445 | * needed because association based subselect requires "WHERE" conditional | ||
1446 | * expressions to be injected, but there is no scope to do that. Only scope | ||
1447 | * accessible is "FROM", prohibiting an easy implementation without larger | ||
1448 | * changes.} | ||
1449 | */ | ||
1450 | public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration | ||
1451 | { | ||
1452 | /* | ||
1453 | NOT YET IMPLEMENTED! | ||
1454 | |||
1455 | $glimpse = $this->lexer->glimpse(); | ||
1456 | |||
1457 | if ($glimpse->type == TokenType::T_DOT) { | ||
1458 | $associationPathExpression = $this->AssociationPathExpression(); | ||
1459 | |||
1460 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1461 | $this->match(TokenType::T_AS); | ||
1462 | } | ||
1463 | |||
1464 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1465 | $identificationVariable = $associationPathExpression->identificationVariable; | ||
1466 | $field = $associationPathExpression->associationField; | ||
1467 | |||
1468 | $class = $this->queryComponents[$identificationVariable]['metadata']; | ||
1469 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']); | ||
1470 | |||
1471 | // Building queryComponent | ||
1472 | $joinQueryComponent = array( | ||
1473 | 'metadata' => $targetClass, | ||
1474 | 'parent' => $identificationVariable, | ||
1475 | 'relation' => $class->getAssociationMapping($field), | ||
1476 | 'map' => null, | ||
1477 | 'nestingLevel' => $this->nestingLevel, | ||
1478 | 'token' => $this->lexer->lookahead | ||
1479 | ); | ||
1480 | |||
1481 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
1482 | |||
1483 | return new AST\SubselectIdentificationVariableDeclaration( | ||
1484 | $associationPathExpression, $aliasIdentificationVariable | ||
1485 | ); | ||
1486 | } | ||
1487 | */ | ||
1488 | |||
1489 | return $this->IdentificationVariableDeclaration(); | ||
1490 | } | ||
1491 | |||
1492 | /** | ||
1493 | * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" | ||
1494 | * (JoinAssociationDeclaration | RangeVariableDeclaration) | ||
1495 | * ["WITH" ConditionalExpression] | ||
1496 | */ | ||
1497 | public function Join(): AST\Join | ||
1498 | { | ||
1499 | // Check Join type | ||
1500 | $joinType = AST\Join::JOIN_TYPE_INNER; | ||
1501 | |||
1502 | switch (true) { | ||
1503 | case $this->lexer->isNextToken(TokenType::T_LEFT): | ||
1504 | $this->match(TokenType::T_LEFT); | ||
1505 | |||
1506 | $joinType = AST\Join::JOIN_TYPE_LEFT; | ||
1507 | |||
1508 | // Possible LEFT OUTER join | ||
1509 | if ($this->lexer->isNextToken(TokenType::T_OUTER)) { | ||
1510 | $this->match(TokenType::T_OUTER); | ||
1511 | |||
1512 | $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; | ||
1513 | } | ||
1514 | |||
1515 | break; | ||
1516 | |||
1517 | case $this->lexer->isNextToken(TokenType::T_INNER): | ||
1518 | $this->match(TokenType::T_INNER); | ||
1519 | break; | ||
1520 | |||
1521 | default: | ||
1522 | // Do nothing | ||
1523 | } | ||
1524 | |||
1525 | $this->match(TokenType::T_JOIN); | ||
1526 | |||
1527 | $next = $this->lexer->glimpse(); | ||
1528 | assert($next !== null); | ||
1529 | $joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration(); | ||
1530 | $adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH); | ||
1531 | $join = new AST\Join($joinType, $joinDeclaration); | ||
1532 | |||
1533 | // Describe non-root join declaration | ||
1534 | if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { | ||
1535 | $joinDeclaration->isRoot = false; | ||
1536 | } | ||
1537 | |||
1538 | // Check for ad-hoc Join conditions | ||
1539 | if ($adhocConditions) { | ||
1540 | $this->match(TokenType::T_WITH); | ||
1541 | |||
1542 | $join->conditionalExpression = $this->ConditionalExpression(); | ||
1543 | } | ||
1544 | |||
1545 | return $join; | ||
1546 | } | ||
1547 | |||
1548 | /** | ||
1549 | * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable | ||
1550 | * | ||
1551 | * @throws QueryException | ||
1552 | */ | ||
1553 | public function RangeVariableDeclaration(): AST\RangeVariableDeclaration | ||
1554 | { | ||
1555 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) { | ||
1556 | $this->semanticalError('Subquery is not supported here', $this->lexer->token); | ||
1557 | } | ||
1558 | |||
1559 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
1560 | |||
1561 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
1562 | |||
1563 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1564 | $this->match(TokenType::T_AS); | ||
1565 | } | ||
1566 | |||
1567 | assert($this->lexer->lookahead !== null); | ||
1568 | $token = $this->lexer->lookahead; | ||
1569 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1570 | $classMetadata = $this->em->getClassMetadata($abstractSchemaName); | ||
1571 | |||
1572 | // Building queryComponent | ||
1573 | $queryComponent = [ | ||
1574 | 'metadata' => $classMetadata, | ||
1575 | 'parent' => null, | ||
1576 | 'relation' => null, | ||
1577 | 'map' => null, | ||
1578 | 'nestingLevel' => $this->nestingLevel, | ||
1579 | 'token' => $token, | ||
1580 | ]; | ||
1581 | |||
1582 | $this->queryComponents[$aliasIdentificationVariable] = $queryComponent; | ||
1583 | |||
1584 | return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable); | ||
1585 | } | ||
1586 | |||
1587 | /** | ||
1588 | * JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] | ||
1589 | */ | ||
1590 | public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration | ||
1591 | { | ||
1592 | $joinAssociationPathExpression = $this->JoinAssociationPathExpression(); | ||
1593 | |||
1594 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1595 | $this->match(TokenType::T_AS); | ||
1596 | } | ||
1597 | |||
1598 | assert($this->lexer->lookahead !== null); | ||
1599 | |||
1600 | $aliasIdentificationVariable = $this->AliasIdentificationVariable(); | ||
1601 | $indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null; | ||
1602 | |||
1603 | $identificationVariable = $joinAssociationPathExpression->identificationVariable; | ||
1604 | $field = $joinAssociationPathExpression->associationField; | ||
1605 | |||
1606 | $class = $this->getMetadataForDqlAlias($identificationVariable); | ||
1607 | $targetClass = $this->em->getClassMetadata($class->associationMappings[$field]->targetEntity); | ||
1608 | |||
1609 | // Building queryComponent | ||
1610 | $joinQueryComponent = [ | ||
1611 | 'metadata' => $targetClass, | ||
1612 | 'parent' => $joinAssociationPathExpression->identificationVariable, | ||
1613 | 'relation' => $class->getAssociationMapping($field), | ||
1614 | 'map' => null, | ||
1615 | 'nestingLevel' => $this->nestingLevel, | ||
1616 | 'token' => $this->lexer->lookahead, | ||
1617 | ]; | ||
1618 | |||
1619 | $this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent; | ||
1620 | |||
1621 | return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy); | ||
1622 | } | ||
1623 | |||
1624 | /** | ||
1625 | * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" | ||
1626 | */ | ||
1627 | public function NewObjectExpression(): AST\NewObjectExpression | ||
1628 | { | ||
1629 | $args = []; | ||
1630 | $this->match(TokenType::T_NEW); | ||
1631 | |||
1632 | $className = $this->AbstractSchemaName(); // note that this is not yet validated | ||
1633 | $token = $this->lexer->token; | ||
1634 | |||
1635 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1636 | |||
1637 | $args[] = $this->NewObjectArg(); | ||
1638 | |||
1639 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1640 | $this->match(TokenType::T_COMMA); | ||
1641 | |||
1642 | $args[] = $this->NewObjectArg(); | ||
1643 | } | ||
1644 | |||
1645 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1646 | |||
1647 | $expression = new AST\NewObjectExpression($className, $args); | ||
1648 | |||
1649 | // Defer NewObjectExpression validation | ||
1650 | $this->deferredNewObjectExpressions[] = [ | ||
1651 | 'token' => $token, | ||
1652 | 'expression' => $expression, | ||
1653 | 'nestingLevel' => $this->nestingLevel, | ||
1654 | ]; | ||
1655 | |||
1656 | return $expression; | ||
1657 | } | ||
1658 | |||
1659 | /** | ||
1660 | * NewObjectArg ::= ScalarExpression | "(" Subselect ")" | ||
1661 | */ | ||
1662 | public function NewObjectArg(): mixed | ||
1663 | { | ||
1664 | assert($this->lexer->lookahead !== null); | ||
1665 | $token = $this->lexer->lookahead; | ||
1666 | $peek = $this->lexer->glimpse(); | ||
1667 | |||
1668 | assert($peek !== null); | ||
1669 | if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) { | ||
1670 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1671 | $expression = $this->Subselect(); | ||
1672 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1673 | |||
1674 | return $expression; | ||
1675 | } | ||
1676 | |||
1677 | return $this->ScalarExpression(); | ||
1678 | } | ||
1679 | |||
1680 | /** | ||
1681 | * IndexBy ::= "INDEX" "BY" SingleValuedPathExpression | ||
1682 | */ | ||
1683 | public function IndexBy(): AST\IndexBy | ||
1684 | { | ||
1685 | $this->match(TokenType::T_INDEX); | ||
1686 | $this->match(TokenType::T_BY); | ||
1687 | $pathExpr = $this->SingleValuedPathExpression(); | ||
1688 | |||
1689 | // Add the INDEX BY info to the query component | ||
1690 | $this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field; | ||
1691 | |||
1692 | return new AST\IndexBy($pathExpr); | ||
1693 | } | ||
1694 | |||
1695 | /** | ||
1696 | * ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | | ||
1697 | * StateFieldPathExpression | BooleanPrimary | CaseExpression | | ||
1698 | * InstanceOfExpression | ||
1699 | * | ||
1700 | * @return mixed One of the possible expressions or subexpressions. | ||
1701 | */ | ||
1702 | public function ScalarExpression(): mixed | ||
1703 | { | ||
1704 | assert($this->lexer->token !== null); | ||
1705 | assert($this->lexer->lookahead !== null); | ||
1706 | $lookahead = $this->lexer->lookahead->type; | ||
1707 | $peek = $this->lexer->glimpse(); | ||
1708 | |||
1709 | switch (true) { | ||
1710 | case $lookahead === TokenType::T_INTEGER: | ||
1711 | case $lookahead === TokenType::T_FLOAT: | ||
1712 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 ) | ||
1713 | case $lookahead === TokenType::T_MINUS: | ||
1714 | case $lookahead === TokenType::T_PLUS: | ||
1715 | return $this->SimpleArithmeticExpression(); | ||
1716 | |||
1717 | case $lookahead === TokenType::T_STRING: | ||
1718 | return $this->StringPrimary(); | ||
1719 | |||
1720 | case $lookahead === TokenType::T_TRUE: | ||
1721 | case $lookahead === TokenType::T_FALSE: | ||
1722 | $this->match($lookahead); | ||
1723 | |||
1724 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
1725 | |||
1726 | case $lookahead === TokenType::T_INPUT_PARAMETER: | ||
1727 | return match (true) { | ||
1728 | $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), | ||
1729 | default => $this->InputParameter(), | ||
1730 | }; | ||
1731 | |||
1732 | case $lookahead === TokenType::T_CASE: | ||
1733 | case $lookahead === TokenType::T_COALESCE: | ||
1734 | case $lookahead === TokenType::T_NULLIF: | ||
1735 | // Since NULLIF and COALESCE can be identified as a function, | ||
1736 | // we need to check these before checking for FunctionDeclaration | ||
1737 | return $this->CaseExpression(); | ||
1738 | |||
1739 | case $lookahead === TokenType::T_OPEN_PARENTHESIS: | ||
1740 | return $this->SimpleArithmeticExpression(); | ||
1741 | |||
1742 | // this check must be done before checking for a filed path expression | ||
1743 | case $this->isFunction(): | ||
1744 | $this->lexer->peek(); | ||
1745 | |||
1746 | return match (true) { | ||
1747 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(), | ||
1748 | default => $this->FunctionDeclaration(), | ||
1749 | }; | ||
1750 | |||
1751 | // it is no function, so it must be a field path | ||
1752 | case $lookahead === TokenType::T_IDENTIFIER: | ||
1753 | $this->lexer->peek(); // lookahead => '.' | ||
1754 | $this->lexer->peek(); // lookahead => token after '.' | ||
1755 | $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' | ||
1756 | $this->lexer->resetPeek(); | ||
1757 | |||
1758 | if ($this->isMathOperator($peek)) { | ||
1759 | return $this->SimpleArithmeticExpression(); | ||
1760 | } | ||
1761 | |||
1762 | return $this->StateFieldPathExpression(); | ||
1763 | |||
1764 | default: | ||
1765 | $this->syntaxError(); | ||
1766 | } | ||
1767 | } | ||
1768 | |||
1769 | /** | ||
1770 | * CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression | ||
1771 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
1772 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
1773 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
1774 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
1775 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
1776 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
1777 | * NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
1778 | * | ||
1779 | * @return mixed One of the possible expressions or subexpressions. | ||
1780 | */ | ||
1781 | public function CaseExpression(): mixed | ||
1782 | { | ||
1783 | assert($this->lexer->lookahead !== null); | ||
1784 | $lookahead = $this->lexer->lookahead->type; | ||
1785 | |||
1786 | switch ($lookahead) { | ||
1787 | case TokenType::T_NULLIF: | ||
1788 | return $this->NullIfExpression(); | ||
1789 | |||
1790 | case TokenType::T_COALESCE: | ||
1791 | return $this->CoalesceExpression(); | ||
1792 | |||
1793 | case TokenType::T_CASE: | ||
1794 | $this->lexer->resetPeek(); | ||
1795 | $peek = $this->lexer->peek(); | ||
1796 | |||
1797 | assert($peek !== null); | ||
1798 | if ($peek->type === TokenType::T_WHEN) { | ||
1799 | return $this->GeneralCaseExpression(); | ||
1800 | } | ||
1801 | |||
1802 | return $this->SimpleCaseExpression(); | ||
1803 | |||
1804 | default: | ||
1805 | // Do nothing | ||
1806 | break; | ||
1807 | } | ||
1808 | |||
1809 | $this->syntaxError(); | ||
1810 | } | ||
1811 | |||
1812 | /** | ||
1813 | * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" | ||
1814 | */ | ||
1815 | public function CoalesceExpression(): AST\CoalesceExpression | ||
1816 | { | ||
1817 | $this->match(TokenType::T_COALESCE); | ||
1818 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1819 | |||
1820 | // Process ScalarExpressions (1..N) | ||
1821 | $scalarExpressions = []; | ||
1822 | $scalarExpressions[] = $this->ScalarExpression(); | ||
1823 | |||
1824 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
1825 | $this->match(TokenType::T_COMMA); | ||
1826 | |||
1827 | $scalarExpressions[] = $this->ScalarExpression(); | ||
1828 | } | ||
1829 | |||
1830 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1831 | |||
1832 | return new AST\CoalesceExpression($scalarExpressions); | ||
1833 | } | ||
1834 | |||
1835 | /** | ||
1836 | * NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")" | ||
1837 | */ | ||
1838 | public function NullIfExpression(): AST\NullIfExpression | ||
1839 | { | ||
1840 | $this->match(TokenType::T_NULLIF); | ||
1841 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1842 | |||
1843 | $firstExpression = $this->ScalarExpression(); | ||
1844 | $this->match(TokenType::T_COMMA); | ||
1845 | $secondExpression = $this->ScalarExpression(); | ||
1846 | |||
1847 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1848 | |||
1849 | return new AST\NullIfExpression($firstExpression, $secondExpression); | ||
1850 | } | ||
1851 | |||
1852 | /** | ||
1853 | * GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END" | ||
1854 | */ | ||
1855 | public function GeneralCaseExpression(): AST\GeneralCaseExpression | ||
1856 | { | ||
1857 | $this->match(TokenType::T_CASE); | ||
1858 | |||
1859 | // Process WhenClause (1..N) | ||
1860 | $whenClauses = []; | ||
1861 | |||
1862 | do { | ||
1863 | $whenClauses[] = $this->WhenClause(); | ||
1864 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
1865 | |||
1866 | $this->match(TokenType::T_ELSE); | ||
1867 | $scalarExpression = $this->ScalarExpression(); | ||
1868 | $this->match(TokenType::T_END); | ||
1869 | |||
1870 | return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); | ||
1871 | } | ||
1872 | |||
1873 | /** | ||
1874 | * SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END" | ||
1875 | * CaseOperand ::= StateFieldPathExpression | TypeDiscriminator | ||
1876 | */ | ||
1877 | public function SimpleCaseExpression(): AST\SimpleCaseExpression | ||
1878 | { | ||
1879 | $this->match(TokenType::T_CASE); | ||
1880 | $caseOperand = $this->StateFieldPathExpression(); | ||
1881 | |||
1882 | // Process SimpleWhenClause (1..N) | ||
1883 | $simpleWhenClauses = []; | ||
1884 | |||
1885 | do { | ||
1886 | $simpleWhenClauses[] = $this->SimpleWhenClause(); | ||
1887 | } while ($this->lexer->isNextToken(TokenType::T_WHEN)); | ||
1888 | |||
1889 | $this->match(TokenType::T_ELSE); | ||
1890 | $scalarExpression = $this->ScalarExpression(); | ||
1891 | $this->match(TokenType::T_END); | ||
1892 | |||
1893 | return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); | ||
1894 | } | ||
1895 | |||
1896 | /** | ||
1897 | * WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression | ||
1898 | */ | ||
1899 | public function WhenClause(): AST\WhenClause | ||
1900 | { | ||
1901 | $this->match(TokenType::T_WHEN); | ||
1902 | $conditionalExpression = $this->ConditionalExpression(); | ||
1903 | $this->match(TokenType::T_THEN); | ||
1904 | |||
1905 | return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); | ||
1906 | } | ||
1907 | |||
1908 | /** | ||
1909 | * SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression | ||
1910 | */ | ||
1911 | public function SimpleWhenClause(): AST\SimpleWhenClause | ||
1912 | { | ||
1913 | $this->match(TokenType::T_WHEN); | ||
1914 | $conditionalExpression = $this->ScalarExpression(); | ||
1915 | $this->match(TokenType::T_THEN); | ||
1916 | |||
1917 | return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); | ||
1918 | } | ||
1919 | |||
1920 | /** | ||
1921 | * SelectExpression ::= ( | ||
1922 | * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | | ||
1923 | * "(" Subselect ")" | CaseExpression | NewObjectExpression | ||
1924 | * ) [["AS"] ["HIDDEN"] AliasResultVariable] | ||
1925 | */ | ||
1926 | public function SelectExpression(): AST\SelectExpression | ||
1927 | { | ||
1928 | assert($this->lexer->lookahead !== null); | ||
1929 | $expression = null; | ||
1930 | $identVariable = null; | ||
1931 | $peek = $this->lexer->glimpse(); | ||
1932 | $lookaheadType = $this->lexer->lookahead->type; | ||
1933 | assert($peek !== null); | ||
1934 | |||
1935 | switch (true) { | ||
1936 | // ScalarExpression (u.name) | ||
1937 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT: | ||
1938 | $expression = $this->ScalarExpression(); | ||
1939 | break; | ||
1940 | |||
1941 | // IdentificationVariable (u) | ||
1942 | case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
1943 | $expression = $identVariable = $this->IdentificationVariable(); | ||
1944 | break; | ||
1945 | |||
1946 | // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) | ||
1947 | case $lookaheadType === TokenType::T_CASE: | ||
1948 | case $lookaheadType === TokenType::T_COALESCE: | ||
1949 | case $lookaheadType === TokenType::T_NULLIF: | ||
1950 | $expression = $this->CaseExpression(); | ||
1951 | break; | ||
1952 | |||
1953 | // DQL Function (SUM(u.value) or SUM(u.value) + 1) | ||
1954 | case $this->isFunction(): | ||
1955 | $this->lexer->peek(); // "(" | ||
1956 | |||
1957 | $expression = match (true) { | ||
1958 | $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), | ||
1959 | default => $this->FunctionDeclaration(), | ||
1960 | }; | ||
1961 | |||
1962 | break; | ||
1963 | |||
1964 | // Subselect | ||
1965 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT: | ||
1966 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
1967 | $expression = $this->Subselect(); | ||
1968 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
1969 | break; | ||
1970 | |||
1971 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
1972 | case $lookaheadType === TokenType::T_OPEN_PARENTHESIS: | ||
1973 | case $lookaheadType === TokenType::T_INTEGER: | ||
1974 | case $lookaheadType === TokenType::T_STRING: | ||
1975 | case $lookaheadType === TokenType::T_FLOAT: | ||
1976 | // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) | ||
1977 | case $lookaheadType === TokenType::T_MINUS: | ||
1978 | case $lookaheadType === TokenType::T_PLUS: | ||
1979 | $expression = $this->SimpleArithmeticExpression(); | ||
1980 | break; | ||
1981 | |||
1982 | // NewObjectExpression (New ClassName(id, name)) | ||
1983 | case $lookaheadType === TokenType::T_NEW: | ||
1984 | $expression = $this->NewObjectExpression(); | ||
1985 | break; | ||
1986 | |||
1987 | default: | ||
1988 | $this->syntaxError( | ||
1989 | 'IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | "(" Subselect ")" | CaseExpression', | ||
1990 | $this->lexer->lookahead, | ||
1991 | ); | ||
1992 | } | ||
1993 | |||
1994 | // [["AS"] ["HIDDEN"] AliasResultVariable] | ||
1995 | $mustHaveAliasResultVariable = false; | ||
1996 | |||
1997 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
1998 | $this->match(TokenType::T_AS); | ||
1999 | |||
2000 | $mustHaveAliasResultVariable = true; | ||
2001 | } | ||
2002 | |||
2003 | $hiddenAliasResultVariable = false; | ||
2004 | |||
2005 | if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) { | ||
2006 | $this->match(TokenType::T_HIDDEN); | ||
2007 | |||
2008 | $hiddenAliasResultVariable = true; | ||
2009 | } | ||
2010 | |||
2011 | $aliasResultVariable = null; | ||
2012 | |||
2013 | if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
2014 | assert($expression instanceof AST\Node || is_string($expression)); | ||
2015 | $token = $this->lexer->lookahead; | ||
2016 | $aliasResultVariable = $this->AliasResultVariable(); | ||
2017 | |||
2018 | // Include AliasResultVariable in query components. | ||
2019 | $this->queryComponents[$aliasResultVariable] = [ | ||
2020 | 'resultVariable' => $expression, | ||
2021 | 'nestingLevel' => $this->nestingLevel, | ||
2022 | 'token' => $token, | ||
2023 | ]; | ||
2024 | } | ||
2025 | |||
2026 | // AST | ||
2027 | |||
2028 | $expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable); | ||
2029 | |||
2030 | if ($identVariable) { | ||
2031 | $this->identVariableExpressions[$identVariable] = $expr; | ||
2032 | } | ||
2033 | |||
2034 | return $expr; | ||
2035 | } | ||
2036 | |||
2037 | /** | ||
2038 | * SimpleSelectExpression ::= ( | ||
2039 | * StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | | ||
2040 | * AggregateExpression | "(" Subselect ")" | ScalarExpression | ||
2041 | * ) [["AS"] AliasResultVariable] | ||
2042 | */ | ||
2043 | public function SimpleSelectExpression(): AST\SimpleSelectExpression | ||
2044 | { | ||
2045 | assert($this->lexer->lookahead !== null); | ||
2046 | $peek = $this->lexer->glimpse(); | ||
2047 | assert($peek !== null); | ||
2048 | |||
2049 | switch ($this->lexer->lookahead->type) { | ||
2050 | case TokenType::T_IDENTIFIER: | ||
2051 | switch (true) { | ||
2052 | case $peek->type === TokenType::T_DOT: | ||
2053 | $expression = $this->StateFieldPathExpression(); | ||
2054 | |||
2055 | return new AST\SimpleSelectExpression($expression); | ||
2056 | |||
2057 | case $peek->type !== TokenType::T_OPEN_PARENTHESIS: | ||
2058 | $expression = $this->IdentificationVariable(); | ||
2059 | |||
2060 | return new AST\SimpleSelectExpression($expression); | ||
2061 | |||
2062 | case $this->isFunction(): | ||
2063 | // SUM(u.id) + COUNT(u.id) | ||
2064 | if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) { | ||
2065 | return new AST\SimpleSelectExpression($this->ScalarExpression()); | ||
2066 | } | ||
2067 | |||
2068 | // COUNT(u.id) | ||
2069 | if ($this->isAggregateFunction($this->lexer->lookahead->type)) { | ||
2070 | return new AST\SimpleSelectExpression($this->AggregateExpression()); | ||
2071 | } | ||
2072 | |||
2073 | // IDENTITY(u) | ||
2074 | return new AST\SimpleSelectExpression($this->FunctionDeclaration()); | ||
2075 | |||
2076 | default: | ||
2077 | // Do nothing | ||
2078 | } | ||
2079 | |||
2080 | break; | ||
2081 | |||
2082 | case TokenType::T_OPEN_PARENTHESIS: | ||
2083 | if ($peek->type !== TokenType::T_SELECT) { | ||
2084 | // Shortcut: ScalarExpression => SimpleArithmeticExpression | ||
2085 | $expression = $this->SimpleArithmeticExpression(); | ||
2086 | |||
2087 | return new AST\SimpleSelectExpression($expression); | ||
2088 | } | ||
2089 | |||
2090 | // Subselect | ||
2091 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2092 | $expression = $this->Subselect(); | ||
2093 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2094 | |||
2095 | return new AST\SimpleSelectExpression($expression); | ||
2096 | |||
2097 | default: | ||
2098 | // Do nothing | ||
2099 | } | ||
2100 | |||
2101 | $this->lexer->peek(); | ||
2102 | |||
2103 | $expression = $this->ScalarExpression(); | ||
2104 | $expr = new AST\SimpleSelectExpression($expression); | ||
2105 | |||
2106 | if ($this->lexer->isNextToken(TokenType::T_AS)) { | ||
2107 | $this->match(TokenType::T_AS); | ||
2108 | } | ||
2109 | |||
2110 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) { | ||
2111 | $token = $this->lexer->lookahead; | ||
2112 | $resultVariable = $this->AliasResultVariable(); | ||
2113 | $expr->fieldIdentificationVariable = $resultVariable; | ||
2114 | |||
2115 | // Include AliasResultVariable in query components. | ||
2116 | $this->queryComponents[$resultVariable] = [ | ||
2117 | 'resultvariable' => $expr, | ||
2118 | 'nestingLevel' => $this->nestingLevel, | ||
2119 | 'token' => $token, | ||
2120 | ]; | ||
2121 | } | ||
2122 | |||
2123 | return $expr; | ||
2124 | } | ||
2125 | |||
2126 | /** | ||
2127 | * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* | ||
2128 | */ | ||
2129 | public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
2130 | { | ||
2131 | $conditionalTerms = []; | ||
2132 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
2133 | |||
2134 | while ($this->lexer->isNextToken(TokenType::T_OR)) { | ||
2135 | $this->match(TokenType::T_OR); | ||
2136 | |||
2137 | $conditionalTerms[] = $this->ConditionalTerm(); | ||
2138 | } | ||
2139 | |||
2140 | // Phase 1 AST optimization: Prevent AST\ConditionalExpression | ||
2141 | // if only one AST\ConditionalTerm is defined | ||
2142 | if (count($conditionalTerms) === 1) { | ||
2143 | return $conditionalTerms[0]; | ||
2144 | } | ||
2145 | |||
2146 | return new AST\ConditionalExpression($conditionalTerms); | ||
2147 | } | ||
2148 | |||
2149 | /** | ||
2150 | * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* | ||
2151 | */ | ||
2152 | public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm | ||
2153 | { | ||
2154 | $conditionalFactors = []; | ||
2155 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
2156 | |||
2157 | while ($this->lexer->isNextToken(TokenType::T_AND)) { | ||
2158 | $this->match(TokenType::T_AND); | ||
2159 | |||
2160 | $conditionalFactors[] = $this->ConditionalFactor(); | ||
2161 | } | ||
2162 | |||
2163 | // Phase 1 AST optimization: Prevent AST\ConditionalTerm | ||
2164 | // if only one AST\ConditionalFactor is defined | ||
2165 | if (count($conditionalFactors) === 1) { | ||
2166 | return $conditionalFactors[0]; | ||
2167 | } | ||
2168 | |||
2169 | return new AST\ConditionalTerm($conditionalFactors); | ||
2170 | } | ||
2171 | |||
2172 | /** | ||
2173 | * ConditionalFactor ::= ["NOT"] ConditionalPrimary | ||
2174 | */ | ||
2175 | public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary | ||
2176 | { | ||
2177 | $not = false; | ||
2178 | |||
2179 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2180 | $this->match(TokenType::T_NOT); | ||
2181 | |||
2182 | $not = true; | ||
2183 | } | ||
2184 | |||
2185 | $conditionalPrimary = $this->ConditionalPrimary(); | ||
2186 | |||
2187 | // Phase 1 AST optimization: Prevent AST\ConditionalFactor | ||
2188 | // if only one AST\ConditionalPrimary is defined | ||
2189 | if (! $not) { | ||
2190 | return $conditionalPrimary; | ||
2191 | } | ||
2192 | |||
2193 | return new AST\ConditionalFactor($conditionalPrimary, $not); | ||
2194 | } | ||
2195 | |||
2196 | /** | ||
2197 | * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" | ||
2198 | */ | ||
2199 | public function ConditionalPrimary(): AST\ConditionalPrimary | ||
2200 | { | ||
2201 | $condPrimary = new AST\ConditionalPrimary(); | ||
2202 | |||
2203 | if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2204 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
2205 | |||
2206 | return $condPrimary; | ||
2207 | } | ||
2208 | |||
2209 | // Peek beyond the matching closing parenthesis ')' | ||
2210 | $peek = $this->peekBeyondClosingParenthesis(); | ||
2211 | |||
2212 | if ( | ||
2213 | $peek !== null && ( | ||
2214 | in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) || | ||
2215 | in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) || | ||
2216 | $this->isMathOperator($peek) | ||
2217 | ) | ||
2218 | ) { | ||
2219 | $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); | ||
2220 | |||
2221 | return $condPrimary; | ||
2222 | } | ||
2223 | |||
2224 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2225 | $condPrimary->conditionalExpression = $this->ConditionalExpression(); | ||
2226 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2227 | |||
2228 | return $condPrimary; | ||
2229 | } | ||
2230 | |||
2231 | /** | ||
2232 | * SimpleConditionalExpression ::= | ||
2233 | * ComparisonExpression | BetweenExpression | LikeExpression | | ||
2234 | * InExpression | NullComparisonExpression | ExistsExpression | | ||
2235 | * EmptyCollectionComparisonExpression | CollectionMemberExpression | | ||
2236 | * InstanceOfExpression | ||
2237 | */ | ||
2238 | public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression | ||
2239 | { | ||
2240 | assert($this->lexer->lookahead !== null); | ||
2241 | if ($this->lexer->isNextToken(TokenType::T_EXISTS)) { | ||
2242 | return $this->ExistsExpression(); | ||
2243 | } | ||
2244 | |||
2245 | $token = $this->lexer->lookahead; | ||
2246 | $peek = $this->lexer->glimpse(); | ||
2247 | $lookahead = $token; | ||
2248 | |||
2249 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2250 | $token = $this->lexer->glimpse(); | ||
2251 | } | ||
2252 | |||
2253 | assert($token !== null); | ||
2254 | assert($peek !== null); | ||
2255 | if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) { | ||
2256 | // Peek beyond the matching closing parenthesis. | ||
2257 | $beyond = $this->lexer->peek(); | ||
2258 | |||
2259 | switch ($peek->value) { | ||
2260 | case '(': | ||
2261 | // Peeks beyond the matched closing parenthesis. | ||
2262 | $token = $this->peekBeyondClosingParenthesis(false); | ||
2263 | assert($token !== null); | ||
2264 | |||
2265 | if ($token->type === TokenType::T_NOT) { | ||
2266 | $token = $this->lexer->peek(); | ||
2267 | assert($token !== null); | ||
2268 | } | ||
2269 | |||
2270 | if ($token->type === TokenType::T_IS) { | ||
2271 | $lookahead = $this->lexer->peek(); | ||
2272 | } | ||
2273 | |||
2274 | break; | ||
2275 | |||
2276 | default: | ||
2277 | // Peek beyond the PathExpression or InputParameter. | ||
2278 | $token = $beyond; | ||
2279 | |||
2280 | while ($token->value === '.') { | ||
2281 | $this->lexer->peek(); | ||
2282 | |||
2283 | $token = $this->lexer->peek(); | ||
2284 | assert($token !== null); | ||
2285 | } | ||
2286 | |||
2287 | // Also peek beyond a NOT if there is one. | ||
2288 | assert($token !== null); | ||
2289 | if ($token->type === TokenType::T_NOT) { | ||
2290 | $token = $this->lexer->peek(); | ||
2291 | assert($token !== null); | ||
2292 | } | ||
2293 | |||
2294 | // We need to go even further in case of IS (differentiate between NULL and EMPTY) | ||
2295 | $lookahead = $this->lexer->peek(); | ||
2296 | } | ||
2297 | |||
2298 | assert($lookahead !== null); | ||
2299 | // Also peek beyond a NOT if there is one. | ||
2300 | if ($lookahead->type === TokenType::T_NOT) { | ||
2301 | $lookahead = $this->lexer->peek(); | ||
2302 | } | ||
2303 | |||
2304 | $this->lexer->resetPeek(); | ||
2305 | } | ||
2306 | |||
2307 | if ($token->type === TokenType::T_BETWEEN) { | ||
2308 | return $this->BetweenExpression(); | ||
2309 | } | ||
2310 | |||
2311 | if ($token->type === TokenType::T_LIKE) { | ||
2312 | return $this->LikeExpression(); | ||
2313 | } | ||
2314 | |||
2315 | if ($token->type === TokenType::T_IN) { | ||
2316 | return $this->InExpression(); | ||
2317 | } | ||
2318 | |||
2319 | if ($token->type === TokenType::T_INSTANCE) { | ||
2320 | return $this->InstanceOfExpression(); | ||
2321 | } | ||
2322 | |||
2323 | if ($token->type === TokenType::T_MEMBER) { | ||
2324 | return $this->CollectionMemberExpression(); | ||
2325 | } | ||
2326 | |||
2327 | assert($lookahead !== null); | ||
2328 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) { | ||
2329 | return $this->NullComparisonExpression(); | ||
2330 | } | ||
2331 | |||
2332 | if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) { | ||
2333 | return $this->EmptyCollectionComparisonExpression(); | ||
2334 | } | ||
2335 | |||
2336 | return $this->ComparisonExpression(); | ||
2337 | } | ||
2338 | |||
2339 | /** | ||
2340 | * EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY" | ||
2341 | */ | ||
2342 | public function EmptyCollectionComparisonExpression(): AST\EmptyCollectionComparisonExpression | ||
2343 | { | ||
2344 | $pathExpression = $this->CollectionValuedPathExpression(); | ||
2345 | $this->match(TokenType::T_IS); | ||
2346 | |||
2347 | $not = false; | ||
2348 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2349 | $this->match(TokenType::T_NOT); | ||
2350 | $not = true; | ||
2351 | } | ||
2352 | |||
2353 | $this->match(TokenType::T_EMPTY); | ||
2354 | |||
2355 | return new AST\EmptyCollectionComparisonExpression( | ||
2356 | $pathExpression, | ||
2357 | $not, | ||
2358 | ); | ||
2359 | } | ||
2360 | |||
2361 | /** | ||
2362 | * CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression | ||
2363 | * | ||
2364 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
2365 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
2366 | */ | ||
2367 | public function CollectionMemberExpression(): AST\CollectionMemberExpression | ||
2368 | { | ||
2369 | $not = false; | ||
2370 | $entityExpr = $this->EntityExpression(); | ||
2371 | |||
2372 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2373 | $this->match(TokenType::T_NOT); | ||
2374 | |||
2375 | $not = true; | ||
2376 | } | ||
2377 | |||
2378 | $this->match(TokenType::T_MEMBER); | ||
2379 | |||
2380 | if ($this->lexer->isNextToken(TokenType::T_OF)) { | ||
2381 | $this->match(TokenType::T_OF); | ||
2382 | } | ||
2383 | |||
2384 | return new AST\CollectionMemberExpression( | ||
2385 | $entityExpr, | ||
2386 | $this->CollectionValuedPathExpression(), | ||
2387 | $not, | ||
2388 | ); | ||
2389 | } | ||
2390 | |||
2391 | /** | ||
2392 | * Literal ::= string | char | integer | float | boolean | ||
2393 | */ | ||
2394 | public function Literal(): AST\Literal | ||
2395 | { | ||
2396 | assert($this->lexer->lookahead !== null); | ||
2397 | assert($this->lexer->token !== null); | ||
2398 | switch ($this->lexer->lookahead->type) { | ||
2399 | case TokenType::T_STRING: | ||
2400 | $this->match(TokenType::T_STRING); | ||
2401 | |||
2402 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2403 | |||
2404 | case TokenType::T_INTEGER: | ||
2405 | case TokenType::T_FLOAT: | ||
2406 | $this->match( | ||
2407 | $this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT, | ||
2408 | ); | ||
2409 | |||
2410 | return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value); | ||
2411 | |||
2412 | case TokenType::T_TRUE: | ||
2413 | case TokenType::T_FALSE: | ||
2414 | $this->match( | ||
2415 | $this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE, | ||
2416 | ); | ||
2417 | |||
2418 | return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value); | ||
2419 | |||
2420 | default: | ||
2421 | $this->syntaxError('Literal'); | ||
2422 | } | ||
2423 | } | ||
2424 | |||
2425 | /** | ||
2426 | * InParameter ::= ArithmeticExpression | InputParameter | ||
2427 | */ | ||
2428 | public function InParameter(): AST\InputParameter|AST\ArithmeticExpression | ||
2429 | { | ||
2430 | assert($this->lexer->lookahead !== null); | ||
2431 | if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) { | ||
2432 | return $this->InputParameter(); | ||
2433 | } | ||
2434 | |||
2435 | return $this->ArithmeticExpression(); | ||
2436 | } | ||
2437 | |||
2438 | /** | ||
2439 | * InputParameter ::= PositionalParameter | NamedParameter | ||
2440 | */ | ||
2441 | public function InputParameter(): AST\InputParameter | ||
2442 | { | ||
2443 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2444 | assert($this->lexer->token !== null); | ||
2445 | |||
2446 | return new AST\InputParameter($this->lexer->token->value); | ||
2447 | } | ||
2448 | |||
2449 | /** | ||
2450 | * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" | ||
2451 | */ | ||
2452 | public function ArithmeticExpression(): AST\ArithmeticExpression | ||
2453 | { | ||
2454 | $expr = new AST\ArithmeticExpression(); | ||
2455 | |||
2456 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2457 | $peek = $this->lexer->glimpse(); | ||
2458 | assert($peek !== null); | ||
2459 | |||
2460 | if ($peek->type === TokenType::T_SELECT) { | ||
2461 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2462 | $expr->subselect = $this->Subselect(); | ||
2463 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2464 | |||
2465 | return $expr; | ||
2466 | } | ||
2467 | } | ||
2468 | |||
2469 | $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); | ||
2470 | |||
2471 | return $expr; | ||
2472 | } | ||
2473 | |||
2474 | /** | ||
2475 | * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* | ||
2476 | */ | ||
2477 | public function SimpleArithmeticExpression(): AST\Node|string | ||
2478 | { | ||
2479 | $terms = []; | ||
2480 | $terms[] = $this->ArithmeticTerm(); | ||
2481 | |||
2482 | while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
2483 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
2484 | |||
2485 | assert($this->lexer->token !== null); | ||
2486 | $terms[] = $this->lexer->token->value; | ||
2487 | $terms[] = $this->ArithmeticTerm(); | ||
2488 | } | ||
2489 | |||
2490 | // Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression | ||
2491 | // if only one AST\ArithmeticTerm is defined | ||
2492 | if (count($terms) === 1) { | ||
2493 | return $terms[0]; | ||
2494 | } | ||
2495 | |||
2496 | return new AST\SimpleArithmeticExpression($terms); | ||
2497 | } | ||
2498 | |||
2499 | /** | ||
2500 | * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* | ||
2501 | */ | ||
2502 | public function ArithmeticTerm(): AST\Node|string | ||
2503 | { | ||
2504 | $factors = []; | ||
2505 | $factors[] = $this->ArithmeticFactor(); | ||
2506 | |||
2507 | while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) { | ||
2508 | $this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE); | ||
2509 | |||
2510 | assert($this->lexer->token !== null); | ||
2511 | $factors[] = $this->lexer->token->value; | ||
2512 | $factors[] = $this->ArithmeticFactor(); | ||
2513 | } | ||
2514 | |||
2515 | // Phase 1 AST optimization: Prevent AST\ArithmeticTerm | ||
2516 | // if only one AST\ArithmeticFactor is defined | ||
2517 | if (count($factors) === 1) { | ||
2518 | return $factors[0]; | ||
2519 | } | ||
2520 | |||
2521 | return new AST\ArithmeticTerm($factors); | ||
2522 | } | ||
2523 | |||
2524 | /** | ||
2525 | * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary | ||
2526 | */ | ||
2527 | public function ArithmeticFactor(): AST\Node|string|AST\ArithmeticFactor | ||
2528 | { | ||
2529 | $sign = null; | ||
2530 | |||
2531 | $isPlus = $this->lexer->isNextToken(TokenType::T_PLUS); | ||
2532 | if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) { | ||
2533 | $this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS); | ||
2534 | $sign = $isPlus; | ||
2535 | } | ||
2536 | |||
2537 | $primary = $this->ArithmeticPrimary(); | ||
2538 | |||
2539 | // Phase 1 AST optimization: Prevent AST\ArithmeticFactor | ||
2540 | // if only one AST\ArithmeticPrimary is defined | ||
2541 | if ($sign === null) { | ||
2542 | return $primary; | ||
2543 | } | ||
2544 | |||
2545 | return new AST\ArithmeticFactor($primary, $sign); | ||
2546 | } | ||
2547 | |||
2548 | /** | ||
2549 | * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression | ||
2550 | * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings | ||
2551 | * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable | ||
2552 | * | InputParameter | CaseExpression | ||
2553 | */ | ||
2554 | public function ArithmeticPrimary(): AST\Node|string | ||
2555 | { | ||
2556 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { | ||
2557 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2558 | |||
2559 | $expr = $this->SimpleArithmeticExpression(); | ||
2560 | |||
2561 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2562 | |||
2563 | return new AST\ParenthesisExpression($expr); | ||
2564 | } | ||
2565 | |||
2566 | if ($this->lexer->lookahead === null) { | ||
2567 | $this->syntaxError('ArithmeticPrimary'); | ||
2568 | } | ||
2569 | |||
2570 | switch ($this->lexer->lookahead->type) { | ||
2571 | case TokenType::T_COALESCE: | ||
2572 | case TokenType::T_NULLIF: | ||
2573 | case TokenType::T_CASE: | ||
2574 | return $this->CaseExpression(); | ||
2575 | |||
2576 | case TokenType::T_IDENTIFIER: | ||
2577 | $peek = $this->lexer->glimpse(); | ||
2578 | |||
2579 | if ($peek !== null && $peek->value === '(') { | ||
2580 | return $this->FunctionDeclaration(); | ||
2581 | } | ||
2582 | |||
2583 | if ($peek !== null && $peek->value === '.') { | ||
2584 | return $this->SingleValuedPathExpression(); | ||
2585 | } | ||
2586 | |||
2587 | if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) { | ||
2588 | return $this->ResultVariable(); | ||
2589 | } | ||
2590 | |||
2591 | return $this->StateFieldPathExpression(); | ||
2592 | |||
2593 | case TokenType::T_INPUT_PARAMETER: | ||
2594 | return $this->InputParameter(); | ||
2595 | |||
2596 | default: | ||
2597 | $peek = $this->lexer->glimpse(); | ||
2598 | |||
2599 | if ($peek !== null && $peek->value === '(') { | ||
2600 | return $this->FunctionDeclaration(); | ||
2601 | } | ||
2602 | |||
2603 | return $this->Literal(); | ||
2604 | } | ||
2605 | } | ||
2606 | |||
2607 | /** | ||
2608 | * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" | ||
2609 | */ | ||
2610 | public function StringExpression(): AST\Subselect|AST\Node|string | ||
2611 | { | ||
2612 | $peek = $this->lexer->glimpse(); | ||
2613 | assert($peek !== null); | ||
2614 | |||
2615 | // Subselect | ||
2616 | if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) { | ||
2617 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2618 | $expr = $this->Subselect(); | ||
2619 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2620 | |||
2621 | return $expr; | ||
2622 | } | ||
2623 | |||
2624 | assert($this->lexer->lookahead !== null); | ||
2625 | // ResultVariable (string) | ||
2626 | if ( | ||
2627 | $this->lexer->isNextToken(TokenType::T_IDENTIFIER) && | ||
2628 | isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable']) | ||
2629 | ) { | ||
2630 | return $this->ResultVariable(); | ||
2631 | } | ||
2632 | |||
2633 | return $this->StringPrimary(); | ||
2634 | } | ||
2635 | |||
2636 | /** | ||
2637 | * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression | ||
2638 | */ | ||
2639 | public function StringPrimary(): AST\Node | ||
2640 | { | ||
2641 | assert($this->lexer->lookahead !== null); | ||
2642 | $lookaheadType = $this->lexer->lookahead->type; | ||
2643 | |||
2644 | switch ($lookaheadType) { | ||
2645 | case TokenType::T_IDENTIFIER: | ||
2646 | $peek = $this->lexer->glimpse(); | ||
2647 | assert($peek !== null); | ||
2648 | |||
2649 | if ($peek->value === '.') { | ||
2650 | return $this->StateFieldPathExpression(); | ||
2651 | } | ||
2652 | |||
2653 | if ($peek->value === '(') { | ||
2654 | // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions. | ||
2655 | return $this->FunctionDeclaration(); | ||
2656 | } | ||
2657 | |||
2658 | $this->syntaxError("'.' or '('"); | ||
2659 | break; | ||
2660 | |||
2661 | case TokenType::T_STRING: | ||
2662 | $this->match(TokenType::T_STRING); | ||
2663 | assert($this->lexer->token !== null); | ||
2664 | |||
2665 | return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2666 | |||
2667 | case TokenType::T_INPUT_PARAMETER: | ||
2668 | return $this->InputParameter(); | ||
2669 | |||
2670 | case TokenType::T_CASE: | ||
2671 | case TokenType::T_COALESCE: | ||
2672 | case TokenType::T_NULLIF: | ||
2673 | return $this->CaseExpression(); | ||
2674 | |||
2675 | default: | ||
2676 | assert($lookaheadType !== null); | ||
2677 | if ($this->isAggregateFunction($lookaheadType)) { | ||
2678 | return $this->AggregateExpression(); | ||
2679 | } | ||
2680 | } | ||
2681 | |||
2682 | $this->syntaxError( | ||
2683 | 'StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression', | ||
2684 | ); | ||
2685 | } | ||
2686 | |||
2687 | /** | ||
2688 | * EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression | ||
2689 | */ | ||
2690 | public function EntityExpression(): AST\InputParameter|AST\PathExpression | ||
2691 | { | ||
2692 | $glimpse = $this->lexer->glimpse(); | ||
2693 | assert($glimpse !== null); | ||
2694 | |||
2695 | if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') { | ||
2696 | return $this->SingleValuedAssociationPathExpression(); | ||
2697 | } | ||
2698 | |||
2699 | return $this->SimpleEntityExpression(); | ||
2700 | } | ||
2701 | |||
2702 | /** | ||
2703 | * SimpleEntityExpression ::= IdentificationVariable | InputParameter | ||
2704 | */ | ||
2705 | public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression | ||
2706 | { | ||
2707 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2708 | return $this->InputParameter(); | ||
2709 | } | ||
2710 | |||
2711 | return $this->StateFieldPathExpression(); | ||
2712 | } | ||
2713 | |||
2714 | /** | ||
2715 | * AggregateExpression ::= | ||
2716 | * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")" | ||
2717 | */ | ||
2718 | public function AggregateExpression(): AST\AggregateExpression | ||
2719 | { | ||
2720 | assert($this->lexer->lookahead !== null); | ||
2721 | $lookaheadType = $this->lexer->lookahead->type; | ||
2722 | $isDistinct = false; | ||
2723 | |||
2724 | if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) { | ||
2725 | $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); | ||
2726 | } | ||
2727 | |||
2728 | $this->match($lookaheadType); | ||
2729 | assert($this->lexer->token !== null); | ||
2730 | $functionName = $this->lexer->token->value; | ||
2731 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2732 | |||
2733 | if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) { | ||
2734 | $this->match(TokenType::T_DISTINCT); | ||
2735 | $isDistinct = true; | ||
2736 | } | ||
2737 | |||
2738 | $pathExp = $this->SimpleArithmeticExpression(); | ||
2739 | |||
2740 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2741 | |||
2742 | return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); | ||
2743 | } | ||
2744 | |||
2745 | /** | ||
2746 | * QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")" | ||
2747 | */ | ||
2748 | public function QuantifiedExpression(): AST\QuantifiedExpression | ||
2749 | { | ||
2750 | assert($this->lexer->lookahead !== null); | ||
2751 | $lookaheadType = $this->lexer->lookahead->type; | ||
2752 | $value = $this->lexer->lookahead->value; | ||
2753 | |||
2754 | if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) { | ||
2755 | $this->syntaxError('ALL, ANY or SOME'); | ||
2756 | } | ||
2757 | |||
2758 | $this->match($lookaheadType); | ||
2759 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2760 | |||
2761 | $qExpr = new AST\QuantifiedExpression($this->Subselect()); | ||
2762 | $qExpr->type = $value; | ||
2763 | |||
2764 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2765 | |||
2766 | return $qExpr; | ||
2767 | } | ||
2768 | |||
2769 | /** | ||
2770 | * BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression | ||
2771 | */ | ||
2772 | public function BetweenExpression(): AST\BetweenExpression | ||
2773 | { | ||
2774 | $not = false; | ||
2775 | $arithExpr1 = $this->ArithmeticExpression(); | ||
2776 | |||
2777 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2778 | $this->match(TokenType::T_NOT); | ||
2779 | $not = true; | ||
2780 | } | ||
2781 | |||
2782 | $this->match(TokenType::T_BETWEEN); | ||
2783 | $arithExpr2 = $this->ArithmeticExpression(); | ||
2784 | $this->match(TokenType::T_AND); | ||
2785 | $arithExpr3 = $this->ArithmeticExpression(); | ||
2786 | |||
2787 | return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not); | ||
2788 | } | ||
2789 | |||
2790 | /** | ||
2791 | * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) | ||
2792 | */ | ||
2793 | public function ComparisonExpression(): AST\ComparisonExpression | ||
2794 | { | ||
2795 | $this->lexer->glimpse(); | ||
2796 | |||
2797 | $leftExpr = $this->ArithmeticExpression(); | ||
2798 | $operator = $this->ComparisonOperator(); | ||
2799 | $rightExpr = $this->isNextAllAnySome() | ||
2800 | ? $this->QuantifiedExpression() | ||
2801 | : $this->ArithmeticExpression(); | ||
2802 | |||
2803 | return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); | ||
2804 | } | ||
2805 | |||
2806 | /** | ||
2807 | * InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")" | ||
2808 | */ | ||
2809 | public function InExpression(): AST\InListExpression|AST\InSubselectExpression | ||
2810 | { | ||
2811 | $expression = $this->ArithmeticExpression(); | ||
2812 | |||
2813 | $not = false; | ||
2814 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2815 | $this->match(TokenType::T_NOT); | ||
2816 | $not = true; | ||
2817 | } | ||
2818 | |||
2819 | $this->match(TokenType::T_IN); | ||
2820 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2821 | |||
2822 | if ($this->lexer->isNextToken(TokenType::T_SELECT)) { | ||
2823 | $inExpression = new AST\InSubselectExpression( | ||
2824 | $expression, | ||
2825 | $this->Subselect(), | ||
2826 | $not, | ||
2827 | ); | ||
2828 | } else { | ||
2829 | $literals = [$this->InParameter()]; | ||
2830 | |||
2831 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
2832 | $this->match(TokenType::T_COMMA); | ||
2833 | $literals[] = $this->InParameter(); | ||
2834 | } | ||
2835 | |||
2836 | $inExpression = new AST\InListExpression( | ||
2837 | $expression, | ||
2838 | $literals, | ||
2839 | $not, | ||
2840 | ); | ||
2841 | } | ||
2842 | |||
2843 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2844 | |||
2845 | return $inExpression; | ||
2846 | } | ||
2847 | |||
2848 | /** | ||
2849 | * InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") | ||
2850 | */ | ||
2851 | public function InstanceOfExpression(): AST\InstanceOfExpression | ||
2852 | { | ||
2853 | $identificationVariable = $this->IdentificationVariable(); | ||
2854 | |||
2855 | $not = false; | ||
2856 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2857 | $this->match(TokenType::T_NOT); | ||
2858 | $not = true; | ||
2859 | } | ||
2860 | |||
2861 | $this->match(TokenType::T_INSTANCE); | ||
2862 | $this->match(TokenType::T_OF); | ||
2863 | |||
2864 | $exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) | ||
2865 | ? $this->InstanceOfParameterList() | ||
2866 | : [$this->InstanceOfParameter()]; | ||
2867 | |||
2868 | return new AST\InstanceOfExpression( | ||
2869 | $identificationVariable, | ||
2870 | $exprValues, | ||
2871 | $not, | ||
2872 | ); | ||
2873 | } | ||
2874 | |||
2875 | /** @return non-empty-list<AST\InputParameter|string> */ | ||
2876 | public function InstanceOfParameterList(): array | ||
2877 | { | ||
2878 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
2879 | |||
2880 | $exprValues = [$this->InstanceOfParameter()]; | ||
2881 | |||
2882 | while ($this->lexer->isNextToken(TokenType::T_COMMA)) { | ||
2883 | $this->match(TokenType::T_COMMA); | ||
2884 | |||
2885 | $exprValues[] = $this->InstanceOfParameter(); | ||
2886 | } | ||
2887 | |||
2888 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
2889 | |||
2890 | return $exprValues; | ||
2891 | } | ||
2892 | |||
2893 | /** | ||
2894 | * InstanceOfParameter ::= AbstractSchemaName | InputParameter | ||
2895 | */ | ||
2896 | public function InstanceOfParameter(): AST\InputParameter|string | ||
2897 | { | ||
2898 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2899 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2900 | assert($this->lexer->token !== null); | ||
2901 | |||
2902 | return new AST\InputParameter($this->lexer->token->value); | ||
2903 | } | ||
2904 | |||
2905 | $abstractSchemaName = $this->AbstractSchemaName(); | ||
2906 | |||
2907 | $this->validateAbstractSchemaName($abstractSchemaName); | ||
2908 | |||
2909 | return $abstractSchemaName; | ||
2910 | } | ||
2911 | |||
2912 | /** | ||
2913 | * LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char] | ||
2914 | */ | ||
2915 | public function LikeExpression(): AST\LikeExpression | ||
2916 | { | ||
2917 | $stringExpr = $this->StringExpression(); | ||
2918 | $not = false; | ||
2919 | |||
2920 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
2921 | $this->match(TokenType::T_NOT); | ||
2922 | $not = true; | ||
2923 | } | ||
2924 | |||
2925 | $this->match(TokenType::T_LIKE); | ||
2926 | |||
2927 | if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { | ||
2928 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2929 | assert($this->lexer->token !== null); | ||
2930 | $stringPattern = new AST\InputParameter($this->lexer->token->value); | ||
2931 | } else { | ||
2932 | $stringPattern = $this->StringPrimary(); | ||
2933 | } | ||
2934 | |||
2935 | $escapeChar = null; | ||
2936 | |||
2937 | if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) { | ||
2938 | $this->match(TokenType::T_ESCAPE); | ||
2939 | $this->match(TokenType::T_STRING); | ||
2940 | assert($this->lexer->token !== null); | ||
2941 | |||
2942 | $escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value); | ||
2943 | } | ||
2944 | |||
2945 | return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not); | ||
2946 | } | ||
2947 | |||
2948 | /** | ||
2949 | * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" | ||
2950 | */ | ||
2951 | public function NullComparisonExpression(): AST\NullComparisonExpression | ||
2952 | { | ||
2953 | switch (true) { | ||
2954 | case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER): | ||
2955 | $this->match(TokenType::T_INPUT_PARAMETER); | ||
2956 | assert($this->lexer->token !== null); | ||
2957 | |||
2958 | $expr = new AST\InputParameter($this->lexer->token->value); | ||
2959 | break; | ||
2960 | |||
2961 | case $this->lexer->isNextToken(TokenType::T_NULLIF): | ||
2962 | $expr = $this->NullIfExpression(); | ||
2963 | break; | ||
2964 | |||
2965 | case $this->lexer->isNextToken(TokenType::T_COALESCE): | ||
2966 | $expr = $this->CoalesceExpression(); | ||
2967 | break; | ||
2968 | |||
2969 | case $this->isFunction(): | ||
2970 | $expr = $this->FunctionDeclaration(); | ||
2971 | break; | ||
2972 | |||
2973 | default: | ||
2974 | // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression | ||
2975 | $glimpse = $this->lexer->glimpse(); | ||
2976 | assert($glimpse !== null); | ||
2977 | |||
2978 | if ($glimpse->type === TokenType::T_DOT) { | ||
2979 | $expr = $this->SingleValuedPathExpression(); | ||
2980 | |||
2981 | // Leave switch statement | ||
2982 | break; | ||
2983 | } | ||
2984 | |||
2985 | assert($this->lexer->lookahead !== null); | ||
2986 | $lookaheadValue = $this->lexer->lookahead->value; | ||
2987 | |||
2988 | // Validate existing component | ||
2989 | if (! isset($this->queryComponents[$lookaheadValue])) { | ||
2990 | $this->semanticalError('Cannot add having condition on undefined result variable.'); | ||
2991 | } | ||
2992 | |||
2993 | // Validate SingleValuedPathExpression (ie.: "product") | ||
2994 | if (isset($this->queryComponents[$lookaheadValue]['metadata'])) { | ||
2995 | $expr = $this->SingleValuedPathExpression(); | ||
2996 | break; | ||
2997 | } | ||
2998 | |||
2999 | // Validating ResultVariable | ||
3000 | if (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) { | ||
3001 | $this->semanticalError('Cannot add having condition on a non result variable.'); | ||
3002 | } | ||
3003 | |||
3004 | $expr = $this->ResultVariable(); | ||
3005 | break; | ||
3006 | } | ||
3007 | |||
3008 | $this->match(TokenType::T_IS); | ||
3009 | |||
3010 | $not = false; | ||
3011 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
3012 | $this->match(TokenType::T_NOT); | ||
3013 | |||
3014 | $not = true; | ||
3015 | } | ||
3016 | |||
3017 | $this->match(TokenType::T_NULL); | ||
3018 | |||
3019 | return new AST\NullComparisonExpression($expr, $not); | ||
3020 | } | ||
3021 | |||
3022 | /** | ||
3023 | * ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" | ||
3024 | */ | ||
3025 | public function ExistsExpression(): AST\ExistsExpression | ||
3026 | { | ||
3027 | $not = false; | ||
3028 | |||
3029 | if ($this->lexer->isNextToken(TokenType::T_NOT)) { | ||
3030 | $this->match(TokenType::T_NOT); | ||
3031 | $not = true; | ||
3032 | } | ||
3033 | |||
3034 | $this->match(TokenType::T_EXISTS); | ||
3035 | $this->match(TokenType::T_OPEN_PARENTHESIS); | ||
3036 | |||
3037 | $subselect = $this->Subselect(); | ||
3038 | |||
3039 | $this->match(TokenType::T_CLOSE_PARENTHESIS); | ||
3040 | |||
3041 | return new AST\ExistsExpression($subselect, $not); | ||
3042 | } | ||
3043 | |||
3044 | /** | ||
3045 | * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" | ||
3046 | */ | ||
3047 | public function ComparisonOperator(): string | ||
3048 | { | ||
3049 | assert($this->lexer->lookahead !== null); | ||
3050 | switch ($this->lexer->lookahead->value) { | ||
3051 | case '=': | ||
3052 | $this->match(TokenType::T_EQUALS); | ||
3053 | |||
3054 | return '='; | ||
3055 | |||
3056 | case '<': | ||
3057 | $this->match(TokenType::T_LOWER_THAN); | ||
3058 | $operator = '<'; | ||
3059 | |||
3060 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
3061 | $this->match(TokenType::T_EQUALS); | ||
3062 | $operator .= '='; | ||
3063 | } elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) { | ||
3064 | $this->match(TokenType::T_GREATER_THAN); | ||
3065 | $operator .= '>'; | ||
3066 | } | ||
3067 | |||
3068 | return $operator; | ||
3069 | |||
3070 | case '>': | ||
3071 | $this->match(TokenType::T_GREATER_THAN); | ||
3072 | $operator = '>'; | ||
3073 | |||
3074 | if ($this->lexer->isNextToken(TokenType::T_EQUALS)) { | ||
3075 | $this->match(TokenType::T_EQUALS); | ||
3076 | $operator .= '='; | ||
3077 | } | ||
3078 | |||
3079 | return $operator; | ||
3080 | |||
3081 | case '!': | ||
3082 | $this->match(TokenType::T_NEGATE); | ||
3083 | $this->match(TokenType::T_EQUALS); | ||
3084 | |||
3085 | return '<>'; | ||
3086 | |||
3087 | default: | ||
3088 | $this->syntaxError('=, <, <=, <>, >, >=, !='); | ||
3089 | } | ||
3090 | } | ||
3091 | |||
3092 | /** | ||
3093 | * FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime | ||
3094 | */ | ||
3095 | public function FunctionDeclaration(): Functions\FunctionNode | ||
3096 | { | ||
3097 | assert($this->lexer->lookahead !== null); | ||
3098 | $token = $this->lexer->lookahead; | ||
3099 | $funcName = strtolower($token->value); | ||
3100 | |||
3101 | $customFunctionDeclaration = $this->CustomFunctionDeclaration(); | ||
3102 | |||
3103 | // Check for custom functions functions first! | ||
3104 | switch (true) { | ||
3105 | case $customFunctionDeclaration !== null: | ||
3106 | return $customFunctionDeclaration; | ||
3107 | |||
3108 | case isset(self::$stringFunctions[$funcName]): | ||
3109 | return $this->FunctionsReturningStrings(); | ||
3110 | |||
3111 | case isset(self::$numericFunctions[$funcName]): | ||
3112 | return $this->FunctionsReturningNumerics(); | ||
3113 | |||
3114 | case isset(self::$datetimeFunctions[$funcName]): | ||
3115 | return $this->FunctionsReturningDatetime(); | ||
3116 | |||
3117 | default: | ||
3118 | $this->syntaxError('known function', $token); | ||
3119 | } | ||
3120 | } | ||
3121 | |||
3122 | /** | ||
3123 | * Helper function for FunctionDeclaration grammar rule. | ||
3124 | */ | ||
3125 | private function CustomFunctionDeclaration(): Functions\FunctionNode|null | ||
3126 | { | ||
3127 | assert($this->lexer->lookahead !== null); | ||
3128 | $token = $this->lexer->lookahead; | ||
3129 | $funcName = strtolower($token->value); | ||
3130 | |||
3131 | // Check for custom functions afterwards | ||
3132 | $config = $this->em->getConfiguration(); | ||
3133 | |||
3134 | return match (true) { | ||
3135 | $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(), | ||
3136 | $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(), | ||
3137 | $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(), | ||
3138 | default => null, | ||
3139 | }; | ||
3140 | } | ||
3141 | |||
3142 | /** | ||
3143 | * FunctionsReturningNumerics ::= | ||
3144 | * "LENGTH" "(" StringPrimary ")" | | ||
3145 | * "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" | | ||
3146 | * "ABS" "(" SimpleArithmeticExpression ")" | | ||
3147 | * "SQRT" "(" SimpleArithmeticExpression ")" | | ||
3148 | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
3149 | * "SIZE" "(" CollectionValuedPathExpression ")" | | ||
3150 | * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
3151 | * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | | ||
3152 | * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | ||
3153 | */ | ||
3154 | public function FunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
3155 | { | ||
3156 | assert($this->lexer->lookahead !== null); | ||
3157 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3158 | $funcClass = self::$numericFunctions[$funcNameLower]; | ||
3159 | |||
3160 | $function = new $funcClass($funcNameLower); | ||
3161 | $function->parse($this); | ||
3162 | |||
3163 | return $function; | ||
3164 | } | ||
3165 | |||
3166 | public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode | ||
3167 | { | ||
3168 | assert($this->lexer->lookahead !== null); | ||
3169 | // getCustomNumericFunction is case-insensitive | ||
3170 | $functionName = strtolower($this->lexer->lookahead->value); | ||
3171 | $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName); | ||
3172 | |||
3173 | assert($functionClass !== null); | ||
3174 | |||
3175 | $function = is_string($functionClass) | ||
3176 | ? new $functionClass($functionName) | ||
3177 | : $functionClass($functionName); | ||
3178 | |||
3179 | $function->parse($this); | ||
3180 | |||
3181 | return $function; | ||
3182 | } | ||
3183 | |||
3184 | /** | ||
3185 | * FunctionsReturningDateTime ::= | ||
3186 | * "CURRENT_DATE" | | ||
3187 | * "CURRENT_TIME" | | ||
3188 | * "CURRENT_TIMESTAMP" | | ||
3189 | * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | | ||
3190 | * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | ||
3191 | */ | ||
3192 | public function FunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
3193 | { | ||
3194 | assert($this->lexer->lookahead !== null); | ||
3195 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3196 | $funcClass = self::$datetimeFunctions[$funcNameLower]; | ||
3197 | |||
3198 | $function = new $funcClass($funcNameLower); | ||
3199 | $function->parse($this); | ||
3200 | |||
3201 | return $function; | ||
3202 | } | ||
3203 | |||
3204 | public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode | ||
3205 | { | ||
3206 | assert($this->lexer->lookahead !== null); | ||
3207 | // getCustomDatetimeFunction is case-insensitive | ||
3208 | $functionName = $this->lexer->lookahead->value; | ||
3209 | $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName); | ||
3210 | |||
3211 | assert($functionClass !== null); | ||
3212 | |||
3213 | $function = is_string($functionClass) | ||
3214 | ? new $functionClass($functionName) | ||
3215 | : $functionClass($functionName); | ||
3216 | |||
3217 | $function->parse($this); | ||
3218 | |||
3219 | return $function; | ||
3220 | } | ||
3221 | |||
3222 | /** | ||
3223 | * FunctionsReturningStrings ::= | ||
3224 | * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" | | ||
3225 | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | | ||
3226 | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | | ||
3227 | * "LOWER" "(" StringPrimary ")" | | ||
3228 | * "UPPER" "(" StringPrimary ")" | | ||
3229 | * "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")" | ||
3230 | */ | ||
3231 | public function FunctionsReturningStrings(): AST\Functions\FunctionNode | ||
3232 | { | ||
3233 | assert($this->lexer->lookahead !== null); | ||
3234 | $funcNameLower = strtolower($this->lexer->lookahead->value); | ||
3235 | $funcClass = self::$stringFunctions[$funcNameLower]; | ||
3236 | |||
3237 | $function = new $funcClass($funcNameLower); | ||
3238 | $function->parse($this); | ||
3239 | |||
3240 | return $function; | ||
3241 | } | ||
3242 | |||
3243 | public function CustomFunctionsReturningStrings(): Functions\FunctionNode | ||
3244 | { | ||
3245 | assert($this->lexer->lookahead !== null); | ||
3246 | // getCustomStringFunction is case-insensitive | ||
3247 | $functionName = $this->lexer->lookahead->value; | ||
3248 | $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName); | ||
3249 | |||
3250 | assert($functionClass !== null); | ||
3251 | |||
3252 | $function = is_string($functionClass) | ||
3253 | ? new $functionClass($functionName) | ||
3254 | : $functionClass($functionName); | ||
3255 | |||
3256 | $function->parse($this); | ||
3257 | |||
3258 | return $function; | ||
3259 | } | ||
3260 | |||
3261 | private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata | ||
3262 | { | ||
3263 | if (! isset($this->queryComponents[$dqlAlias]['metadata'])) { | ||
3264 | throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); | ||
3265 | } | ||
3266 | |||
3267 | return $this->queryComponents[$dqlAlias]['metadata']; | ||
3268 | } | ||
3269 | } | ||