summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Query/Parser.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Query/Parser.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-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.php3269
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
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Query;
6
7use Doctrine\Common\Lexer\Token;
8use Doctrine\ORM\EntityManagerInterface;
9use Doctrine\ORM\Mapping\AssociationMapping;
10use Doctrine\ORM\Mapping\ClassMetadata;
11use Doctrine\ORM\Query;
12use Doctrine\ORM\Query\AST\Functions;
13use LogicException;
14use ReflectionClass;
15
16use function array_search;
17use function assert;
18use function class_exists;
19use function count;
20use function implode;
21use function in_array;
22use function interface_exists;
23use function is_string;
24use function sprintf;
25use function str_contains;
26use function strlen;
27use function strpos;
28use function strrpos;
29use function strtolower;
30use 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 */
47final 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}