vendor/doctrine/orm/src/Query/Parser.php line 355
<?phpdeclare(strict_types=1);namespace Doctrine\ORM\Query;use Doctrine\Common\Lexer\Token;use Doctrine\Deprecations\Deprecation;use Doctrine\ORM\EntityManagerInterface;use Doctrine\ORM\Mapping\ClassMetadata;use Doctrine\ORM\Query;use Doctrine\ORM\Query\AST\Functions;use LogicException;use ReflectionClass;use function array_intersect;use function array_search;use function assert;use function class_exists;use function count;use function explode;use function implode;use function in_array;use function interface_exists;use function is_string;use function sprintf;use function str_contains;use function strlen;use function strpos;use function strrpos;use function strtolower;use function substr;/*** An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.* Parses a DQL query, reports any errors in it, and generates an AST.** @psalm-import-type AssociationMapping from ClassMetadata* @psalm-type DqlToken = Token<TokenType::T_*, string>* @psalm-type QueryComponent = array{* metadata?: ClassMetadata<object>,* parent?: string|null,* relation?: AssociationMapping|null,* map?: string|null,* resultVariable?: AST\Node|string,* nestingLevel: int,* token: DqlToken,* }*/class Parser{/*** @readonly Maps BUILT-IN string function names to AST class names.* @psalm-var array<string, class-string<Functions\FunctionNode>>*/private static $stringFunctions = ['concat' => Functions\ConcatFunction::class,'substring' => Functions\SubstringFunction::class,'trim' => Functions\TrimFunction::class,'lower' => Functions\LowerFunction::class,'upper' => Functions\UpperFunction::class,'identity' => Functions\IdentityFunction::class,];/*** @readonly Maps BUILT-IN numeric function names to AST class names.* @psalm-var array<string, class-string<Functions\FunctionNode>>*/private static $numericFunctions = ['length' => Functions\LengthFunction::class,'locate' => Functions\LocateFunction::class,'abs' => Functions\AbsFunction::class,'sqrt' => Functions\SqrtFunction::class,'mod' => Functions\ModFunction::class,'size' => Functions\SizeFunction::class,'date_diff' => Functions\DateDiffFunction::class,'bit_and' => Functions\BitAndFunction::class,'bit_or' => Functions\BitOrFunction::class,// Aggregate functions'min' => Functions\MinFunction::class,'max' => Functions\MaxFunction::class,'avg' => Functions\AvgFunction::class,'sum' => Functions\SumFunction::class,'count' => Functions\CountFunction::class,];/*** @readonly Maps BUILT-IN datetime function names to AST class names.* @psalm-var array<string, class-string<Functions\FunctionNode>>*/private static $datetimeFunctions = ['current_date' => Functions\CurrentDateFunction::class,'current_time' => Functions\CurrentTimeFunction::class,'current_timestamp' => Functions\CurrentTimestampFunction::class,'date_add' => Functions\DateAddFunction::class,'date_sub' => Functions\DateSubFunction::class,];/** Expressions that were encountered during parsing of identifiers and expressions* and still need to be validated.*//** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */private $deferredIdentificationVariables = [];/** @psalm-var list<array{token: DqlToken|null, expression: AST\PartialObjectExpression, nestingLevel: int}> */private $deferredPartialObjectExpressions = [];/** @psalm-var list<array{token: DqlToken|null, expression: AST\PathExpression, nestingLevel: int}> */private $deferredPathExpressions = [];/** @psalm-var list<array{token: DqlToken|null, expression: mixed, nestingLevel: int}> */private $deferredResultVariables = [];/** @psalm-var list<array{token: DqlToken|null, expression: AST\NewObjectExpression, nestingLevel: int}> */private $deferredNewObjectExpressions = [];/*** The lexer.** @var Lexer*/private $lexer;/*** The parser result.** @var ParserResult*/private $parserResult;/*** The EntityManager.** @var EntityManagerInterface*/private $em;/*** The Query to parse.** @var Query*/private $query;/*** Map of declared query components in the parsed query.** @psalm-var array<string, QueryComponent>*/private $queryComponents = [];/*** Keeps the nesting level of defined ResultVariables.** @var int*/private $nestingLevel = 0;/*** Any additional custom tree walkers that modify the AST.** @psalm-var list<class-string<TreeWalker>>*/private $customTreeWalkers = [];/*** The custom last tree walker, if any, that is responsible for producing the output.** @var class-string<SqlWalker>|null*/private $customOutputWalker;/** @psalm-var array<string, AST\SelectExpression> */private $identVariableExpressions = [];/*** Creates a new query parser object.** @param Query $query The Query to parse.*/public function __construct(Query $query){$this->query = $query;$this->em = $query->getEntityManager();$this->lexer = new Lexer((string) $query->getDQL());$this->parserResult = new ParserResult();}/*** Sets a custom tree walker that produces output.* This tree walker will be run last over the AST, after any other walkers.** @param string $className* @psalm-param class-string<SqlWalker> $className** @return void*/public function setCustomOutputTreeWalker($className){$this->customOutputWalker = $className;}/*** Adds a custom tree walker for modifying the AST.** @param string $className* @psalm-param class-string<TreeWalker> $className** @return void*/public function addCustomTreeWalker($className){$this->customTreeWalkers[] = $className;}/*** Gets the lexer used by the parser.** @return Lexer*/public function getLexer(){return $this->lexer;}/*** Gets the ParserResult that is being filled with information during parsing.** @return ParserResult*/public function getParserResult(){return $this->parserResult;}/*** Gets the EntityManager used by the parser.** @return EntityManagerInterface*/public function getEntityManager(){return $this->em;}/*** Parses and builds AST for the given Query.** @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement*/public function getAST(){// Parse & build AST$AST = $this->QueryLanguage();// Process any deferred validations of some nodes in the AST.// This also allows post-processing of the AST for modification purposes.$this->processDeferredIdentificationVariables();if ($this->deferredPartialObjectExpressions) {$this->processDeferredPartialObjectExpressions();}if ($this->deferredPathExpressions) {$this->processDeferredPathExpressions();}if ($this->deferredResultVariables) {$this->processDeferredResultVariables();}if ($this->deferredNewObjectExpressions) {$this->processDeferredNewObjectExpressions($AST);}$this->processRootEntityAliasSelected();// TODO: Is there a way to remove this? It may impact the mixed hydration resultset a lot!$this->fixIdentificationVariableOrder($AST);return $AST;}/*** Attempts to match the given token with the current lookahead token.** If they match, updates the lookahead token; otherwise raises a syntax* error.** @param TokenType::T_* $token The token type.** @return void** @throws QueryException If the tokens don't match.*/public function match($token){$lookaheadType = $this->lexer->lookahead->type ?? null;// Short-circuit on first condition, usually types matchif ($lookaheadType === $token) {$this->lexer->moveNext();return;}// If parameter is not identifier (1-99) must be exact matchif ($token < TokenType::T_IDENTIFIER) {$this->syntaxError($this->lexer->getLiteral($token));}// If parameter is keyword (200+) must be exact matchif ($token > TokenType::T_IDENTIFIER) {$this->syntaxError($this->lexer->getLiteral($token));}// If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)if ($token === TokenType::T_IDENTIFIER && $lookaheadType < TokenType::T_IDENTIFIER) {$this->syntaxError($this->lexer->getLiteral($token));}$this->lexer->moveNext();}/*** Frees this parser, enabling it to be reused.** @param bool $deep Whether to clean peek and reset errors.* @param int $position Position to reset.** @return void*/public function free($deep = false, $position = 0){// WARNING! Use this method with care. It resets the scanner!$this->lexer->resetPosition($position);// Deep = true cleans peek and also any previously defined errorsif ($deep) {$this->lexer->resetPeek();}$this->lexer->token = null;$this->lexer->lookahead = null;}/*** Parses a query string.** @return ParserResult*/public function parse(){$AST = $this->getAST();$customWalkers = $this->query->getHint(Query::HINT_CUSTOM_TREE_WALKERS);if ($customWalkers !== false) {$this->customTreeWalkers = $customWalkers;}$customOutputWalker = $this->query->getHint(Query::HINT_CUSTOM_OUTPUT_WALKER);if ($customOutputWalker !== false) {$this->customOutputWalker = $customOutputWalker;}// Run any custom tree walkers over the ASTif ($this->customTreeWalkers) {$treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents);foreach ($this->customTreeWalkers as $walker) {$treeWalkerChain->addTreeWalker($walker);}switch (true) {case $AST instanceof AST\UpdateStatement:$treeWalkerChain->walkUpdateStatement($AST);break;case $AST instanceof AST\DeleteStatement:$treeWalkerChain->walkDeleteStatement($AST);break;case $AST instanceof AST\SelectStatement:default:$treeWalkerChain->walkSelectStatement($AST);}$this->queryComponents = $treeWalkerChain->getQueryComponents();}$outputWalkerClass = $this->customOutputWalker ?: SqlWalker::class;$outputWalker = new $outputWalkerClass($this->query, $this->parserResult, $this->queryComponents);// Assign an SQL executor to the parser result$this->parserResult->setSqlExecutor($outputWalker->getExecutor($AST));return $this->parserResult;}/*** Fixes order of identification variables.** They have to appear in the select clause in the same order as the* declarations (from ... x join ... y join ... z ...) appear in the query* as the hydration process relies on that order for proper operation.** @param AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $AST*/private function fixIdentificationVariableOrder(AST\Node $AST): void{if (count($this->identVariableExpressions) <= 1) {return;}assert($AST instanceof AST\SelectStatement);foreach ($this->queryComponents as $dqlAlias => $qComp) {if (! isset($this->identVariableExpressions[$dqlAlias])) {continue;}$expr = $this->identVariableExpressions[$dqlAlias];$key = array_search($expr, $AST->selectClause->selectExpressions, true);unset($AST->selectClause->selectExpressions[$key]);$AST->selectClause->selectExpressions[] = $expr;}}/*** Generates a new syntax error.** @param string $expected Expected string.* @param mixed[]|null $token Got token.* @psalm-param DqlToken|null $token** @return void* @psalm-return no-return** @throws QueryException*/public function syntaxError($expected = '', $token = null){if ($token === null) {$token = $this->lexer->lookahead;}$tokenPos = $token->position ?? '-1';$message = sprintf('line 0, col %d: Error: ', $tokenPos);$message .= $expected !== '' ? sprintf('Expected %s, got ', $expected) : 'Unexpected ';$message .= $this->lexer->lookahead === null ? 'end of string.' : sprintf("'%s'", $token->value);throw QueryException::syntaxError($message, QueryException::dqlError($this->query->getDQL() ?? ''));}/*** Generates a new semantical error.** @param string $message Optional message.* @param mixed[]|null $token Optional token.* @psalm-param DqlToken|null $token** @return void* @psalm-return no-return** @throws QueryException*/public function semanticalError($message = '', $token = null){if ($token === null) {$token = $this->lexer->lookahead ?? new Token('fake token', 42, 0);}// Minimum exposed chars ahead of token$distance = 12;// Find a position of a final word to display in error string$dql = $this->query->getDQL();$length = strlen($dql);$pos = $token->position + $distance;$pos = strpos($dql, ' ', $length > $pos ? $pos : $length);$length = $pos !== false ? $pos - $token->position : $distance;$tokenPos = $token->position > 0 ? $token->position : '-1';$tokenStr = substr($dql, $token->position, $length);// Building informative message$message = 'line 0, col ' . $tokenPos . " near '" . $tokenStr . "': Error: " . $message;throw QueryException::semanticalError($message, QueryException::dqlError($this->query->getDQL()));}/*** Peeks beyond the matched closing parenthesis and returns the first token after that one.** @param bool $resetPeek Reset peek after finding the closing parenthesis.** @return mixed[]* @psalm-return DqlToken|null*/private function peekBeyondClosingParenthesis(bool $resetPeek = true){$token = $this->lexer->peek();$numUnmatched = 1;while ($numUnmatched > 0 && $token !== null) {switch ($token->type) {case TokenType::T_OPEN_PARENTHESIS:++$numUnmatched;break;case TokenType::T_CLOSE_PARENTHESIS:--$numUnmatched;break;default:// Do nothing}$token = $this->lexer->peek();}if ($resetPeek) {$this->lexer->resetPeek();}return $token;}/*** Checks if the given token indicates a mathematical operator.** @psalm-param DqlToken|null $token*/private function isMathOperator($token): bool{return $token !== null && in_array($token->type, [TokenType::T_PLUS, TokenType::T_MINUS, TokenType::T_DIVIDE, TokenType::T_MULTIPLY], true);}/*** Checks if the next-next (after lookahead) token starts a function.** @return bool TRUE if the next-next tokens start a function, FALSE otherwise.*/private function isFunction(): bool{assert($this->lexer->lookahead !== null);$lookaheadType = $this->lexer->lookahead->type;$peek = $this->lexer->peek();$this->lexer->resetPeek();return $lookaheadType >= TokenType::T_IDENTIFIER && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS;}/*** Checks whether the given token type indicates an aggregate function.** @psalm-param TokenType::T_* $tokenType** @return bool TRUE if the token type is an aggregate function, FALSE otherwise.*/private function isAggregateFunction(int $tokenType): bool{return in_array($tokenType,[TokenType::T_AVG, TokenType::T_MIN, TokenType::T_MAX, TokenType::T_SUM, TokenType::T_COUNT],true);}/*** Checks whether the current lookahead token of the lexer has the type T_ALL, T_ANY or T_SOME.*/private function isNextAllAnySome(): bool{assert($this->lexer->lookahead !== null);return in_array($this->lexer->lookahead->type,[TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME],true);}/*** Validates that the given <tt>IdentificationVariable</tt> is semantically correct.* It must exist in query components list.*/private function processDeferredIdentificationVariables(): void{foreach ($this->deferredIdentificationVariables as $deferredItem) {$identVariable = $deferredItem['expression'];// Check if IdentificationVariable exists in queryComponentsif (! isset($this->queryComponents[$identVariable])) {$this->semanticalError(sprintf("'%s' is not defined.", $identVariable),$deferredItem['token']);}$qComp = $this->queryComponents[$identVariable];// Check if queryComponent points to an AbstractSchemaName or a ResultVariableif (! isset($qComp['metadata'])) {$this->semanticalError(sprintf("'%s' does not point to a Class.", $identVariable),$deferredItem['token']);}// Validate if identification variable nesting level is lower or equal than the current oneif ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {$this->semanticalError(sprintf("'%s' is used outside the scope of its declaration.", $identVariable),$deferredItem['token']);}}}/*** Validates that the given <tt>NewObjectExpression</tt>.*/private function processDeferredNewObjectExpressions(AST\SelectStatement $AST): void{foreach ($this->deferredNewObjectExpressions as $deferredItem) {$expression = $deferredItem['expression'];$token = $deferredItem['token'];$className = $expression->className;$args = $expression->args;$fromClassName = $AST->fromClause->identificationVariableDeclarations[0]->rangeVariableDeclaration->abstractSchemaName ?? null;// If the namespace is not given then assumes the first FROM entity namespaceif (! str_contains($className, '\\') && ! class_exists($className) && is_string($fromClassName) && str_contains($fromClassName, '\\')) {$namespace = substr($fromClassName, 0, strrpos($fromClassName, '\\'));$fqcn = $namespace . '\\' . $className;if (class_exists($fqcn)) {$expression->className = $fqcn;$className = $fqcn;}}if (! class_exists($className)) {$this->semanticalError(sprintf('Class "%s" is not defined.', $className), $token);}$class = new ReflectionClass($className);if (! $class->isInstantiable()) {$this->semanticalError(sprintf('Class "%s" can not be instantiated.', $className), $token);}if ($class->getConstructor() === null) {$this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token);}if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) {$this->semanticalError(sprintf('Number of arguments does not match with "%s" constructor declaration.', $className), $token);}}}/*** Validates that the given <tt>PartialObjectExpression</tt> is semantically correct.* It must exist in query components list.*/private function processDeferredPartialObjectExpressions(): void{foreach ($this->deferredPartialObjectExpressions as $deferredItem) {$expr = $deferredItem['expression'];$class = $this->getMetadataForDqlAlias($expr->identificationVariable);foreach ($expr->partialFieldSet as $field) {if (isset($class->fieldMappings[$field])) {continue;}if (isset($class->associationMappings[$field]) &&$class->associationMappings[$field]['isOwningSide'] &&$class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) {continue;}$this->semanticalError(sprintf("There is no mapped field named '%s' on class %s.",$field,$class->name), $deferredItem['token']);}if (array_intersect($class->identifier, $expr->partialFieldSet) !== $class->identifier) {$this->semanticalError('The partial field selection of class ' . $class->name . ' must contain the identifier.',$deferredItem['token']);}}}/*** Validates that the given <tt>ResultVariable</tt> is semantically correct.* It must exist in query components list.*/private function processDeferredResultVariables(): void{foreach ($this->deferredResultVariables as $deferredItem) {$resultVariable = $deferredItem['expression'];// Check if ResultVariable exists in queryComponentsif (! isset($this->queryComponents[$resultVariable])) {$this->semanticalError(sprintf("'%s' is not defined.", $resultVariable),$deferredItem['token']);}$qComp = $this->queryComponents[$resultVariable];// Check if queryComponent points to an AbstractSchemaName or a ResultVariableif (! isset($qComp['resultVariable'])) {$this->semanticalError(sprintf("'%s' does not point to a ResultVariable.", $resultVariable),$deferredItem['token']);}// Validate if identification variable nesting level is lower or equal than the current oneif ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) {$this->semanticalError(sprintf("'%s' is used outside the scope of its declaration.", $resultVariable),$deferredItem['token']);}}}/*** Validates that the given <tt>PathExpression</tt> is semantically correct for grammar rules:** AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression* SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression* StateFieldPathExpression ::= IdentificationVariable "." StateField* SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField* CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField*/private function processDeferredPathExpressions(): void{foreach ($this->deferredPathExpressions as $deferredItem) {$pathExpression = $deferredItem['expression'];$class = $this->getMetadataForDqlAlias($pathExpression->identificationVariable);$field = $pathExpression->field;if ($field === null) {$field = $pathExpression->field = $class->identifier[0];}// Check if field or association existsif (! isset($class->associationMappings[$field]) && ! isset($class->fieldMappings[$field])) {$this->semanticalError('Class ' . $class->name . ' has no field or association named ' . $field,$deferredItem['token']);}$fieldType = AST\PathExpression::TYPE_STATE_FIELD;if (isset($class->associationMappings[$field])) {$assoc = $class->associationMappings[$field];$fieldType = $assoc['type'] & ClassMetadata::TO_ONE? AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION;}// Validate if PathExpression is one of the expected types$expectedType = $pathExpression->expectedType;if (! ($expectedType & $fieldType)) {// We need to recognize which was expected type(s)$expectedStringTypes = [];// Validate state field typeif ($expectedType & AST\PathExpression::TYPE_STATE_FIELD) {$expectedStringTypes[] = 'StateFieldPathExpression';}// Validate single valued association (*-to-one)if ($expectedType & AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) {$expectedStringTypes[] = 'SingleValuedAssociationField';}// Validate single valued association (*-to-many)if ($expectedType & AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) {$expectedStringTypes[] = 'CollectionValuedAssociationField';}// Build the error message$semanticalError = 'Invalid PathExpression. ';$semanticalError .= count($expectedStringTypes) === 1? 'Must be a ' . $expectedStringTypes[0] . '.': implode(' or ', $expectedStringTypes) . ' expected.';$this->semanticalError($semanticalError, $deferredItem['token']);}// We need to force the type in PathExpression$pathExpression->type = $fieldType;}}private function processRootEntityAliasSelected(): void{if (! count($this->identVariableExpressions)) {return;}foreach ($this->identVariableExpressions as $dqlAlias => $expr) {if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) {return;}}$this->semanticalError('Cannot select entity through identification variables without choosing at least one root entity alias.');}/*** QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement** @return AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement*/public function QueryLanguage(){$statement = null;$this->lexer->moveNext();switch ($this->lexer->lookahead->type ?? null) {case TokenType::T_SELECT:$statement = $this->SelectStatement();break;case TokenType::T_UPDATE:$statement = $this->UpdateStatement();break;case TokenType::T_DELETE:$statement = $this->DeleteStatement();break;default:$this->syntaxError('SELECT, UPDATE or DELETE');break;}// Check for end of stringif ($this->lexer->lookahead !== null) {$this->syntaxError('end of string');}return $statement;}/*** SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]** @return AST\SelectStatement*/public function SelectStatement(){$selectStatement = new AST\SelectStatement($this->SelectClause(), $this->FromClause());$selectStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;$selectStatement->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;$selectStatement->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;$selectStatement->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;return $selectStatement;}/*** UpdateStatement ::= UpdateClause [WhereClause]** @return AST\UpdateStatement*/public function UpdateStatement(){$updateStatement = new AST\UpdateStatement($this->UpdateClause());$updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;return $updateStatement;}/*** DeleteStatement ::= DeleteClause [WhereClause]** @return AST\DeleteStatement*/public function DeleteStatement(){$deleteStatement = new AST\DeleteStatement($this->DeleteClause());$deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;return $deleteStatement;}/*** IdentificationVariable ::= identifier** @return string*/public function IdentificationVariable(){$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$identVariable = $this->lexer->token->value;$this->deferredIdentificationVariables[] = ['expression' => $identVariable,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->token,];return $identVariable;}/*** AliasIdentificationVariable = identifier** @return string*/public function AliasIdentificationVariable(){$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$aliasIdentVariable = $this->lexer->token->value;$exists = isset($this->queryComponents[$aliasIdentVariable]);if ($exists) {$this->semanticalError(sprintf("'%s' is already defined.", $aliasIdentVariable),$this->lexer->token);}return $aliasIdentVariable;}/*** AbstractSchemaName ::= fully_qualified_name | aliased_name | identifier** @return string*/public function AbstractSchemaName(){if ($this->lexer->isNextToken(TokenType::T_FULLY_QUALIFIED_NAME)) {$this->match(TokenType::T_FULLY_QUALIFIED_NAME);assert($this->lexer->token !== null);return $this->lexer->token->value;}if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);return $this->lexer->token->value;}$this->match(TokenType::T_ALIASED_NAME);assert($this->lexer->token !== null);Deprecation::trigger('doctrine/orm','https://github.com/doctrine/orm/issues/8818','Short namespace aliases such as "%s" are deprecated and will be removed in Doctrine ORM 3.0.',$this->lexer->token->value);[$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token->value);return $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;}/*** Validates an AbstractSchemaName, making sure the class exists.** @param string $schemaName The name to validate.** @throws QueryException if the name does not exist.*/private function validateAbstractSchemaName(string $schemaName): void{assert($this->lexer->token !== null);if (! (class_exists($schemaName, true) || interface_exists($schemaName, true))) {$this->semanticalError(sprintf("Class '%s' is not defined.", $schemaName),$this->lexer->token);}}/*** AliasResultVariable ::= identifier** @return string*/public function AliasResultVariable(){$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$resultVariable = $this->lexer->token->value;$exists = isset($this->queryComponents[$resultVariable]);if ($exists) {$this->semanticalError(sprintf("'%s' is already defined.", $resultVariable),$this->lexer->token);}return $resultVariable;}/*** ResultVariable ::= identifier** @return string*/public function ResultVariable(){$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$resultVariable = $this->lexer->token->value;// Defer ResultVariable validation$this->deferredResultVariables[] = ['expression' => $resultVariable,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->token,];return $resultVariable;}/*** JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField)** @return AST\JoinAssociationPathExpression*/public function JoinAssociationPathExpression(){$identVariable = $this->IdentificationVariable();if (! isset($this->queryComponents[$identVariable])) {$this->semanticalError('Identification Variable ' . $identVariable . ' used in join path expression but was not defined before.');}$this->match(TokenType::T_DOT);$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$field = $this->lexer->token->value;// Validate association field$class = $this->getMetadataForDqlAlias($identVariable);if (! $class->hasAssociation($field)) {$this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);}return new AST\JoinAssociationPathExpression($identVariable, $field);}/*** Parses an arbitrary path expression and defers semantical validation* based on expected types.** PathExpression ::= IdentificationVariable {"." identifier}*** @param int $expectedTypes* @psalm-param int-mask-of<AST\PathExpression::TYPE_*> $expectedTypes** @return AST\PathExpression*/public function PathExpression($expectedTypes){$identVariable = $this->IdentificationVariable();$field = null;assert($this->lexer->token !== null);if ($this->lexer->isNextToken(TokenType::T_DOT)) {$this->match(TokenType::T_DOT);$this->match(TokenType::T_IDENTIFIER);$field = $this->lexer->token->value;while ($this->lexer->isNextToken(TokenType::T_DOT)) {$this->match(TokenType::T_DOT);$this->match(TokenType::T_IDENTIFIER);$field .= '.' . $this->lexer->token->value;}}// Creating AST node$pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field);// Defer PathExpression validation if requested to be deferred$this->deferredPathExpressions[] = ['expression' => $pathExpr,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->token,];return $pathExpr;}/*** AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression** @return AST\PathExpression*/public function AssociationPathExpression(){return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION |AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);}/*** SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression** @return AST\PathExpression*/public function SingleValuedPathExpression(){return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD |AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);}/*** StateFieldPathExpression ::= IdentificationVariable "." StateField** @return AST\PathExpression*/public function StateFieldPathExpression(){return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD);}/*** SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField** @return AST\PathExpression*/public function SingleValuedAssociationPathExpression(){return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION);}/*** CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField** @return AST\PathExpression*/public function CollectionValuedPathExpression(){return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);}/*** SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}** @return AST\SelectClause*/public function SelectClause(){$isDistinct = false;$this->match(TokenType::T_SELECT);// Check for DISTINCTif ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {$this->match(TokenType::T_DISTINCT);$isDistinct = true;}// Process SelectExpressions (1..N)$selectExpressions = [];$selectExpressions[] = $this->SelectExpression();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$selectExpressions[] = $this->SelectExpression();}return new AST\SelectClause($selectExpressions, $isDistinct);}/*** SimpleSelectClause ::= "SELECT" ["DISTINCT"] SimpleSelectExpression** @return AST\SimpleSelectClause*/public function SimpleSelectClause(){$isDistinct = false;$this->match(TokenType::T_SELECT);if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {$this->match(TokenType::T_DISTINCT);$isDistinct = true;}return new AST\SimpleSelectClause($this->SimpleSelectExpression(), $isDistinct);}/*** UpdateClause ::= "UPDATE" AbstractSchemaName ["AS"] AliasIdentificationVariable "SET" UpdateItem {"," UpdateItem}*** @return AST\UpdateClause*/public function UpdateClause(){$this->match(TokenType::T_UPDATE);assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$abstractSchemaName = $this->AbstractSchemaName();$this->validateAbstractSchemaName($abstractSchemaName);if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}$aliasIdentificationVariable = $this->AliasIdentificationVariable();$class = $this->em->getClassMetadata($abstractSchemaName);// Building queryComponent$queryComponent = ['metadata' => $class,'parent' => null,'relation' => null,'map' => null,'nestingLevel' => $this->nestingLevel,'token' => $token,];$this->queryComponents[$aliasIdentificationVariable] = $queryComponent;$this->match(TokenType::T_SET);$updateItems = [];$updateItems[] = $this->UpdateItem();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$updateItems[] = $this->UpdateItem();}$updateClause = new AST\UpdateClause($abstractSchemaName, $updateItems);$updateClause->aliasIdentificationVariable = $aliasIdentificationVariable;return $updateClause;}/*** DeleteClause ::= "DELETE" ["FROM"] AbstractSchemaName ["AS"] AliasIdentificationVariable** @return AST\DeleteClause*/public function DeleteClause(){$this->match(TokenType::T_DELETE);if ($this->lexer->isNextToken(TokenType::T_FROM)) {$this->match(TokenType::T_FROM);}assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$abstractSchemaName = $this->AbstractSchemaName();$this->validateAbstractSchemaName($abstractSchemaName);$deleteClause = new AST\DeleteClause($abstractSchemaName);if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}$aliasIdentificationVariable = $this->lexer->isNextToken(TokenType::T_IDENTIFIER)? $this->AliasIdentificationVariable(): 'alias_should_have_been_set';$deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;$class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);// Building queryComponent$queryComponent = ['metadata' => $class,'parent' => null,'relation' => null,'map' => null,'nestingLevel' => $this->nestingLevel,'token' => $token,];$this->queryComponents[$aliasIdentificationVariable] = $queryComponent;return $deleteClause;}/*** FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration}*** @return AST\FromClause*/public function FromClause(){$this->match(TokenType::T_FROM);$identificationVariableDeclarations = [];$identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$identificationVariableDeclarations[] = $this->IdentificationVariableDeclaration();}return new AST\FromClause($identificationVariableDeclarations);}/*** SubselectFromClause ::= "FROM" SubselectIdentificationVariableDeclaration {"," SubselectIdentificationVariableDeclaration}*** @return AST\SubselectFromClause*/public function SubselectFromClause(){$this->match(TokenType::T_FROM);$identificationVariables = [];$identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$identificationVariables[] = $this->SubselectIdentificationVariableDeclaration();}return new AST\SubselectFromClause($identificationVariables);}/*** WhereClause ::= "WHERE" ConditionalExpression** @return AST\WhereClause*/public function WhereClause(){$this->match(TokenType::T_WHERE);return new AST\WhereClause($this->ConditionalExpression());}/*** HavingClause ::= "HAVING" ConditionalExpression** @return AST\HavingClause*/public function HavingClause(){$this->match(TokenType::T_HAVING);return new AST\HavingClause($this->ConditionalExpression());}/*** GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}*** @return AST\GroupByClause*/public function GroupByClause(){$this->match(TokenType::T_GROUP);$this->match(TokenType::T_BY);$groupByItems = [$this->GroupByItem()];while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$groupByItems[] = $this->GroupByItem();}return new AST\GroupByClause($groupByItems);}/*** OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*** @return AST\OrderByClause*/public function OrderByClause(){$this->match(TokenType::T_ORDER);$this->match(TokenType::T_BY);$orderByItems = [];$orderByItems[] = $this->OrderByItem();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$orderByItems[] = $this->OrderByItem();}return new AST\OrderByClause($orderByItems);}/*** Subselect ::= SimpleSelectClause SubselectFromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause]** @return AST\Subselect*/public function Subselect(){// Increase query nesting level$this->nestingLevel++;$subselect = new AST\Subselect($this->SimpleSelectClause(), $this->SubselectFromClause());$subselect->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null;$subselect->groupByClause = $this->lexer->isNextToken(TokenType::T_GROUP) ? $this->GroupByClause() : null;$subselect->havingClause = $this->lexer->isNextToken(TokenType::T_HAVING) ? $this->HavingClause() : null;$subselect->orderByClause = $this->lexer->isNextToken(TokenType::T_ORDER) ? $this->OrderByClause() : null;// Decrease query nesting level$this->nestingLevel--;return $subselect;}/*** UpdateItem ::= SingleValuedPathExpression "=" NewValue** @return AST\UpdateItem*/public function UpdateItem(){$pathExpr = $this->SingleValuedPathExpression();$this->match(TokenType::T_EQUALS);return new AST\UpdateItem($pathExpr, $this->NewValue());}/*** GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression** @return string|AST\PathExpression*/public function GroupByItem(){// We need to check if we are in a IdentificationVariable or SingleValuedPathExpression$glimpse = $this->lexer->glimpse();if ($glimpse !== null && $glimpse->type === TokenType::T_DOT) {return $this->SingleValuedPathExpression();}assert($this->lexer->lookahead !== null);// Still need to decide between IdentificationVariable or ResultVariable$lookaheadValue = $this->lexer->lookahead->value;if (! isset($this->queryComponents[$lookaheadValue])) {$this->semanticalError('Cannot group by undefined identification or result variable.');}return isset($this->queryComponents[$lookaheadValue]['metadata'])? $this->IdentificationVariable(): $this->ResultVariable();}/*** OrderByItem ::= (* SimpleArithmeticExpression | SingleValuedPathExpression | CaseExpression |* ScalarExpression | ResultVariable | FunctionDeclaration* ) ["ASC" | "DESC"]** @return AST\OrderByItem*/public function OrderByItem(){$this->lexer->peek(); // lookahead => '.'$this->lexer->peek(); // lookahead => token after '.'$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'$this->lexer->resetPeek();$glimpse = $this->lexer->glimpse();assert($this->lexer->lookahead !== null);switch (true) {case $this->isMathOperator($peek):$expr = $this->SimpleArithmeticExpression();break;case $glimpse !== null && $glimpse->type === TokenType::T_DOT:$expr = $this->SingleValuedPathExpression();break;case $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()):$expr = $this->ScalarExpression();break;case $this->lexer->lookahead->type === TokenType::T_CASE:$expr = $this->CaseExpression();break;case $this->isFunction():$expr = $this->FunctionDeclaration();break;default:$expr = $this->ResultVariable();break;}$type = 'ASC';$item = new AST\OrderByItem($expr);switch (true) {case $this->lexer->isNextToken(TokenType::T_DESC):$this->match(TokenType::T_DESC);$type = 'DESC';break;case $this->lexer->isNextToken(TokenType::T_ASC):$this->match(TokenType::T_ASC);break;default:// Do nothing}$item->type = $type;return $item;}/*** NewValue ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | BooleanPrimary |* EnumPrimary | SimpleEntityExpression | "NULL"** NOTE: Since it is not possible to correctly recognize individual types, here is the full* grammar that needs to be supported:** NewValue ::= SimpleArithmeticExpression | "NULL"** SimpleArithmeticExpression covers all *Primary grammar rules and also SimpleEntityExpression** @return AST\ArithmeticExpression|AST\InputParameter|null*/public function NewValue(){if ($this->lexer->isNextToken(TokenType::T_NULL)) {$this->match(TokenType::T_NULL);return null;}if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {$this->match(TokenType::T_INPUT_PARAMETER);assert($this->lexer->token !== null);return new AST\InputParameter($this->lexer->token->value);}return $this->ArithmeticExpression();}/*** IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}*** @return AST\IdentificationVariableDeclaration*/public function IdentificationVariableDeclaration(){$joins = [];$rangeVariableDeclaration = $this->RangeVariableDeclaration();$indexBy = $this->lexer->isNextToken(TokenType::T_INDEX)? $this->IndexBy(): null;$rangeVariableDeclaration->isRoot = true;while ($this->lexer->isNextToken(TokenType::T_LEFT) ||$this->lexer->isNextToken(TokenType::T_INNER) ||$this->lexer->isNextToken(TokenType::T_JOIN)) {$joins[] = $this->Join();}return new AST\IdentificationVariableDeclaration($rangeVariableDeclaration,$indexBy,$joins);}/*** SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration** {Internal note: WARNING: Solution is harder than a bare implementation.* Desired EBNF support:** SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable)** It demands that entire SQL generation to become programmatical. This is* needed because association based subselect requires "WHERE" conditional* expressions to be injected, but there is no scope to do that. Only scope* accessible is "FROM", prohibiting an easy implementation without larger* changes.}** @return AST\IdentificationVariableDeclaration*/public function SubselectIdentificationVariableDeclaration(){/*NOT YET IMPLEMENTED!$glimpse = $this->lexer->glimpse();if ($glimpse->type == TokenType::T_DOT) {$associationPathExpression = $this->AssociationPathExpression();if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}$aliasIdentificationVariable = $this->AliasIdentificationVariable();$identificationVariable = $associationPathExpression->identificationVariable;$field = $associationPathExpression->associationField;$class = $this->queryComponents[$identificationVariable]['metadata'];$targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);// Building queryComponent$joinQueryComponent = array('metadata' => $targetClass,'parent' => $identificationVariable,'relation' => $class->getAssociationMapping($field),'map' => null,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->lookahead);$this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;return new AST\SubselectIdentificationVariableDeclaration($associationPathExpression, $aliasIdentificationVariable);}*/return $this->IdentificationVariableDeclaration();}/*** Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"* (JoinAssociationDeclaration | RangeVariableDeclaration)* ["WITH" ConditionalExpression]** @return AST\Join*/public function Join(){// Check Join type$joinType = AST\Join::JOIN_TYPE_INNER;switch (true) {case $this->lexer->isNextToken(TokenType::T_LEFT):$this->match(TokenType::T_LEFT);$joinType = AST\Join::JOIN_TYPE_LEFT;// Possible LEFT OUTER joinif ($this->lexer->isNextToken(TokenType::T_OUTER)) {$this->match(TokenType::T_OUTER);$joinType = AST\Join::JOIN_TYPE_LEFTOUTER;}break;case $this->lexer->isNextToken(TokenType::T_INNER):$this->match(TokenType::T_INNER);break;default:// Do nothing}$this->match(TokenType::T_JOIN);$next = $this->lexer->glimpse();assert($next !== null);$joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();$adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);$join = new AST\Join($joinType, $joinDeclaration);// Describe non-root join declarationif ($joinDeclaration instanceof AST\RangeVariableDeclaration) {$joinDeclaration->isRoot = false;}// Check for ad-hoc Join conditionsif ($adhocConditions) {$this->match(TokenType::T_WITH);$join->conditionalExpression = $this->ConditionalExpression();}return $join;}/*** RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable** @return AST\RangeVariableDeclaration** @throws QueryException*/public function RangeVariableDeclaration(){if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $this->lexer->glimpse()->type === TokenType::T_SELECT) {$this->semanticalError('Subquery is not supported here', $this->lexer->token);}$abstractSchemaName = $this->AbstractSchemaName();$this->validateAbstractSchemaName($abstractSchemaName);if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$aliasIdentificationVariable = $this->AliasIdentificationVariable();$classMetadata = $this->em->getClassMetadata($abstractSchemaName);// Building queryComponent$queryComponent = ['metadata' => $classMetadata,'parent' => null,'relation' => null,'map' => null,'nestingLevel' => $this->nestingLevel,'token' => $token,];$this->queryComponents[$aliasIdentificationVariable] = $queryComponent;return new AST\RangeVariableDeclaration($abstractSchemaName, $aliasIdentificationVariable);}/*** JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]** @return AST\JoinAssociationDeclaration*/public function JoinAssociationDeclaration(){$joinAssociationPathExpression = $this->JoinAssociationPathExpression();if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}assert($this->lexer->lookahead !== null);$aliasIdentificationVariable = $this->AliasIdentificationVariable();$indexBy = $this->lexer->isNextToken(TokenType::T_INDEX) ? $this->IndexBy() : null;$identificationVariable = $joinAssociationPathExpression->identificationVariable;$field = $joinAssociationPathExpression->associationField;$class = $this->getMetadataForDqlAlias($identificationVariable);$targetClass = $this->em->getClassMetadata($class->associationMappings[$field]['targetEntity']);// Building queryComponent$joinQueryComponent = ['metadata' => $targetClass,'parent' => $joinAssociationPathExpression->identificationVariable,'relation' => $class->getAssociationMapping($field),'map' => null,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->lookahead,];$this->queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;return new AST\JoinAssociationDeclaration($joinAssociationPathExpression, $aliasIdentificationVariable, $indexBy);}/*** PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet* PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"** @return AST\PartialObjectExpression*/public function PartialObjectExpression(){Deprecation::trigger('doctrine/orm','https://github.com/doctrine/orm/issues/8471','PARTIAL syntax in DQL is deprecated.');$this->match(TokenType::T_PARTIAL);$partialFieldSet = [];$identificationVariable = $this->IdentificationVariable();$this->match(TokenType::T_DOT);$this->match(TokenType::T_OPEN_CURLY_BRACE);$this->match(TokenType::T_IDENTIFIER);assert($this->lexer->token !== null);$field = $this->lexer->token->value;// First field in partial expression might be embeddable propertywhile ($this->lexer->isNextToken(TokenType::T_DOT)) {$this->match(TokenType::T_DOT);$this->match(TokenType::T_IDENTIFIER);$field .= '.' . $this->lexer->token->value;}$partialFieldSet[] = $field;while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$this->match(TokenType::T_IDENTIFIER);$field = $this->lexer->token->value;while ($this->lexer->isNextToken(TokenType::T_DOT)) {$this->match(TokenType::T_DOT);$this->match(TokenType::T_IDENTIFIER);$field .= '.' . $this->lexer->token->value;}$partialFieldSet[] = $field;}$this->match(TokenType::T_CLOSE_CURLY_BRACE);$partialObjectExpression = new AST\PartialObjectExpression($identificationVariable, $partialFieldSet);// Defer PartialObjectExpression validation$this->deferredPartialObjectExpressions[] = ['expression' => $partialObjectExpression,'nestingLevel' => $this->nestingLevel,'token' => $this->lexer->token,];return $partialObjectExpression;}/*** NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"** @return AST\NewObjectExpression*/public function NewObjectExpression(){$this->match(TokenType::T_NEW);$className = $this->AbstractSchemaName(); // note that this is not yet validated$token = $this->lexer->token;$this->match(TokenType::T_OPEN_PARENTHESIS);$args[] = $this->NewObjectArg();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$args[] = $this->NewObjectArg();}$this->match(TokenType::T_CLOSE_PARENTHESIS);$expression = new AST\NewObjectExpression($className, $args);// Defer NewObjectExpression validation$this->deferredNewObjectExpressions[] = ['token' => $token,'expression' => $expression,'nestingLevel' => $this->nestingLevel,];return $expression;}/*** NewObjectArg ::= ScalarExpression | "(" Subselect ")"** @return mixed*/public function NewObjectArg(){assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$peek = $this->lexer->glimpse();assert($peek !== null);if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {$this->match(TokenType::T_OPEN_PARENTHESIS);$expression = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);return $expression;}return $this->ScalarExpression();}/*** IndexBy ::= "INDEX" "BY" SingleValuedPathExpression** @return AST\IndexBy*/public function IndexBy(){$this->match(TokenType::T_INDEX);$this->match(TokenType::T_BY);$pathExpr = $this->SingleValuedPathExpression();// Add the INDEX BY info to the query component$this->queryComponents[$pathExpr->identificationVariable]['map'] = $pathExpr->field;return new AST\IndexBy($pathExpr);}/*** ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |* StateFieldPathExpression | BooleanPrimary | CaseExpression |* InstanceOfExpression** @return mixed One of the possible expressions or subexpressions.*/public function ScalarExpression(){assert($this->lexer->token !== null);assert($this->lexer->lookahead !== null);$lookahead = $this->lexer->lookahead->type;$peek = $this->lexer->glimpse();switch (true) {case $lookahead === TokenType::T_INTEGER:case $lookahead === TokenType::T_FLOAT:// SimpleArithmeticExpression : (- u.value ) or ( + u.value ) or ( - 1 ) or ( + 1 )case $lookahead === TokenType::T_MINUS:case $lookahead === TokenType::T_PLUS:return $this->SimpleArithmeticExpression();case $lookahead === TokenType::T_STRING:return $this->StringPrimary();case $lookahead === TokenType::T_TRUE:case $lookahead === TokenType::T_FALSE:$this->match($lookahead);return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);case $lookahead === TokenType::T_INPUT_PARAMETER:switch (true) {case $this->isMathOperator($peek):// :param + u.valuereturn $this->SimpleArithmeticExpression();default:return $this->InputParameter();}case $lookahead === TokenType::T_CASE:case $lookahead === TokenType::T_COALESCE:case $lookahead === TokenType::T_NULLIF:// Since NULLIF and COALESCE can be identified as a function,// we need to check these before checking for FunctionDeclarationreturn $this->CaseExpression();case $lookahead === TokenType::T_OPEN_PARENTHESIS:return $this->SimpleArithmeticExpression();// this check must be done before checking for a filed path expressioncase $this->isFunction():$this->lexer->peek(); // "("switch (true) {case $this->isMathOperator($this->peekBeyondClosingParenthesis()):// SUM(u.id) + COUNT(u.id)return $this->SimpleArithmeticExpression();default:// IDENTITY(u)return $this->FunctionDeclaration();}break;// it is no function, so it must be a field pathcase $lookahead === TokenType::T_IDENTIFIER:$this->lexer->peek(); // lookahead => '.'$this->lexer->peek(); // lookahead => token after '.'$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'$this->lexer->resetPeek();if ($this->isMathOperator($peek)) {return $this->SimpleArithmeticExpression();}return $this->StateFieldPathExpression();default:$this->syntaxError();}}/*** CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression* GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"* WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator* SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression* CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"* NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"** @return mixed One of the possible expressions or subexpressions.*/public function CaseExpression(){assert($this->lexer->lookahead !== null);$lookahead = $this->lexer->lookahead->type;switch ($lookahead) {case TokenType::T_NULLIF:return $this->NullIfExpression();case TokenType::T_COALESCE:return $this->CoalesceExpression();case TokenType::T_CASE:$this->lexer->resetPeek();$peek = $this->lexer->peek();assert($peek !== null);if ($peek->type === TokenType::T_WHEN) {return $this->GeneralCaseExpression();}return $this->SimpleCaseExpression();default:// Do nothingbreak;}$this->syntaxError();}/*** CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"** @return AST\CoalesceExpression*/public function CoalesceExpression(){$this->match(TokenType::T_COALESCE);$this->match(TokenType::T_OPEN_PARENTHESIS);// Process ScalarExpressions (1..N)$scalarExpressions = [];$scalarExpressions[] = $this->ScalarExpression();while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$scalarExpressions[] = $this->ScalarExpression();}$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\CoalesceExpression($scalarExpressions);}/*** NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"** @return AST\NullIfExpression*/public function NullIfExpression(){$this->match(TokenType::T_NULLIF);$this->match(TokenType::T_OPEN_PARENTHESIS);$firstExpression = $this->ScalarExpression();$this->match(TokenType::T_COMMA);$secondExpression = $this->ScalarExpression();$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\NullIfExpression($firstExpression, $secondExpression);}/*** GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"** @return AST\GeneralCaseExpression*/public function GeneralCaseExpression(){$this->match(TokenType::T_CASE);// Process WhenClause (1..N)$whenClauses = [];do {$whenClauses[] = $this->WhenClause();} while ($this->lexer->isNextToken(TokenType::T_WHEN));$this->match(TokenType::T_ELSE);$scalarExpression = $this->ScalarExpression();$this->match(TokenType::T_END);return new AST\GeneralCaseExpression($whenClauses, $scalarExpression);}/*** SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator** @return AST\SimpleCaseExpression*/public function SimpleCaseExpression(){$this->match(TokenType::T_CASE);$caseOperand = $this->StateFieldPathExpression();// Process SimpleWhenClause (1..N)$simpleWhenClauses = [];do {$simpleWhenClauses[] = $this->SimpleWhenClause();} while ($this->lexer->isNextToken(TokenType::T_WHEN));$this->match(TokenType::T_ELSE);$scalarExpression = $this->ScalarExpression();$this->match(TokenType::T_END);return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression);}/*** WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression** @return AST\WhenClause*/public function WhenClause(){$this->match(TokenType::T_WHEN);$conditionalExpression = $this->ConditionalExpression();$this->match(TokenType::T_THEN);return new AST\WhenClause($conditionalExpression, $this->ScalarExpression());}/*** SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression** @return AST\SimpleWhenClause*/public function SimpleWhenClause(){$this->match(TokenType::T_WHEN);$conditionalExpression = $this->ScalarExpression();$this->match(TokenType::T_THEN);return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression());}/*** SelectExpression ::= (* IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration |* PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression* ) [["AS"] ["HIDDEN"] AliasResultVariable]** @return AST\SelectExpression*/public function SelectExpression(){assert($this->lexer->lookahead !== null);$expression = null;$identVariable = null;$peek = $this->lexer->glimpse();$lookaheadType = $this->lexer->lookahead->type;assert($peek !== null);switch (true) {// ScalarExpression (u.name)case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type === TokenType::T_DOT:$expression = $this->ScalarExpression();break;// IdentificationVariable (u)case $lookaheadType === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_OPEN_PARENTHESIS:$expression = $identVariable = $this->IdentificationVariable();break;// CaseExpression (CASE ... or NULLIF(...) or COALESCE(...))case $lookaheadType === TokenType::T_CASE:case $lookaheadType === TokenType::T_COALESCE:case $lookaheadType === TokenType::T_NULLIF:$expression = $this->CaseExpression();break;// DQL Function (SUM(u.value) or SUM(u.value) + 1)case $this->isFunction():$this->lexer->peek(); // "("switch (true) {case $this->isMathOperator($this->peekBeyondClosingParenthesis()):// SUM(u.id) + COUNT(u.id)$expression = $this->ScalarExpression();break;default:// IDENTITY(u)$expression = $this->FunctionDeclaration();break;}break;// PartialObjectExpression (PARTIAL u.{id, name})case $lookaheadType === TokenType::T_PARTIAL:$expression = $this->PartialObjectExpression();$identVariable = $expression->identificationVariable;break;// Subselectcase $lookaheadType === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT:$this->match(TokenType::T_OPEN_PARENTHESIS);$expression = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);break;// Shortcut: ScalarExpression => SimpleArithmeticExpressioncase $lookaheadType === TokenType::T_OPEN_PARENTHESIS:case $lookaheadType === TokenType::T_INTEGER:case $lookaheadType === TokenType::T_STRING:case $lookaheadType === TokenType::T_FLOAT:// SimpleArithmeticExpression : (- u.value ) or ( + u.value )case $lookaheadType === TokenType::T_MINUS:case $lookaheadType === TokenType::T_PLUS:$expression = $this->SimpleArithmeticExpression();break;// NewObjectExpression (New ClassName(id, name))case $lookaheadType === TokenType::T_NEW:$expression = $this->NewObjectExpression();break;default:$this->syntaxError('IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression',$this->lexer->lookahead);}// [["AS"] ["HIDDEN"] AliasResultVariable]$mustHaveAliasResultVariable = false;if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);$mustHaveAliasResultVariable = true;}$hiddenAliasResultVariable = false;if ($this->lexer->isNextToken(TokenType::T_HIDDEN)) {$this->match(TokenType::T_HIDDEN);$hiddenAliasResultVariable = true;}$aliasResultVariable = null;if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {assert($expression instanceof AST\Node || is_string($expression));$token = $this->lexer->lookahead;$aliasResultVariable = $this->AliasResultVariable();// Include AliasResultVariable in query components.$this->queryComponents[$aliasResultVariable] = ['resultVariable' => $expression,'nestingLevel' => $this->nestingLevel,'token' => $token,];}// AST$expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);if ($identVariable) {$this->identVariableExpressions[$identVariable] = $expr;}return $expr;}/*** SimpleSelectExpression ::= (* StateFieldPathExpression | IdentificationVariable | FunctionDeclaration |* AggregateExpression | "(" Subselect ")" | ScalarExpression* ) [["AS"] AliasResultVariable]** @return AST\SimpleSelectExpression*/public function SimpleSelectExpression(){assert($this->lexer->lookahead !== null);$peek = $this->lexer->glimpse();assert($peek !== null);switch ($this->lexer->lookahead->type) {case TokenType::T_IDENTIFIER:switch (true) {case $peek->type === TokenType::T_DOT:$expression = $this->StateFieldPathExpression();return new AST\SimpleSelectExpression($expression);case $peek->type !== TokenType::T_OPEN_PARENTHESIS:$expression = $this->IdentificationVariable();return new AST\SimpleSelectExpression($expression);case $this->isFunction():// SUM(u.id) + COUNT(u.id)if ($this->isMathOperator($this->peekBeyondClosingParenthesis())) {return new AST\SimpleSelectExpression($this->ScalarExpression());}// COUNT(u.id)if ($this->isAggregateFunction($this->lexer->lookahead->type)) {return new AST\SimpleSelectExpression($this->AggregateExpression());}// IDENTITY(u)return new AST\SimpleSelectExpression($this->FunctionDeclaration());default:// Do nothing}break;case TokenType::T_OPEN_PARENTHESIS:if ($peek->type !== TokenType::T_SELECT) {// Shortcut: ScalarExpression => SimpleArithmeticExpression$expression = $this->SimpleArithmeticExpression();return new AST\SimpleSelectExpression($expression);}// Subselect$this->match(TokenType::T_OPEN_PARENTHESIS);$expression = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\SimpleSelectExpression($expression);default:// Do nothing}$this->lexer->peek();$expression = $this->ScalarExpression();$expr = new AST\SimpleSelectExpression($expression);if ($this->lexer->isNextToken(TokenType::T_AS)) {$this->match(TokenType::T_AS);}if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {$token = $this->lexer->lookahead;$resultVariable = $this->AliasResultVariable();$expr->fieldIdentificationVariable = $resultVariable;// Include AliasResultVariable in query components.$this->queryComponents[$resultVariable] = ['resultvariable' => $expr,'nestingLevel' => $this->nestingLevel,'token' => $token,];}return $expr;}/*** ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}*** @return AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm*/public function ConditionalExpression(){$conditionalTerms = [];$conditionalTerms[] = $this->ConditionalTerm();while ($this->lexer->isNextToken(TokenType::T_OR)) {$this->match(TokenType::T_OR);$conditionalTerms[] = $this->ConditionalTerm();}// Phase 1 AST optimization: Prevent AST\ConditionalExpression// if only one AST\ConditionalTerm is definedif (count($conditionalTerms) === 1) {return $conditionalTerms[0];}return new AST\ConditionalExpression($conditionalTerms);}/*** ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}*** @return AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm*/public function ConditionalTerm(){$conditionalFactors = [];$conditionalFactors[] = $this->ConditionalFactor();while ($this->lexer->isNextToken(TokenType::T_AND)) {$this->match(TokenType::T_AND);$conditionalFactors[] = $this->ConditionalFactor();}// Phase 1 AST optimization: Prevent AST\ConditionalTerm// if only one AST\ConditionalFactor is definedif (count($conditionalFactors) === 1) {return $conditionalFactors[0];}return new AST\ConditionalTerm($conditionalFactors);}/*** ConditionalFactor ::= ["NOT"] ConditionalPrimary** @return AST\ConditionalFactor|AST\ConditionalPrimary*/public function ConditionalFactor(){$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$conditionalPrimary = $this->ConditionalPrimary();// Phase 1 AST optimization: Prevent AST\ConditionalFactor// if only one AST\ConditionalPrimary is definedif (! $not) {return $conditionalPrimary;}return new AST\ConditionalFactor($conditionalPrimary, $not);}/*** ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")"** @return AST\ConditionalPrimary*/public function ConditionalPrimary(){$condPrimary = new AST\ConditionalPrimary();if (! $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {$condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();return $condPrimary;}// Peek beyond the matching closing parenthesis ')'$peek = $this->peekBeyondClosingParenthesis();if ($peek !== null && (in_array($peek->value, ['=', '<', '<=', '<>', '>', '>=', '!='], true) ||in_array($peek->type, [TokenType::T_NOT, TokenType::T_BETWEEN, TokenType::T_LIKE, TokenType::T_IN, TokenType::T_IS, TokenType::T_EXISTS], true) ||$this->isMathOperator($peek))) {$condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression();return $condPrimary;}$this->match(TokenType::T_OPEN_PARENTHESIS);$condPrimary->conditionalExpression = $this->ConditionalExpression();$this->match(TokenType::T_CLOSE_PARENTHESIS);return $condPrimary;}/*** SimpleConditionalExpression ::=* ComparisonExpression | BetweenExpression | LikeExpression |* InExpression | NullComparisonExpression | ExistsExpression |* EmptyCollectionComparisonExpression | CollectionMemberExpression |* InstanceOfExpression** @return (AST\BetweenExpression|* AST\CollectionMemberExpression|* AST\ComparisonExpression|* AST\EmptyCollectionComparisonExpression|* AST\ExistsExpression|* AST\InExpression|* AST\InstanceOfExpression|* AST\LikeExpression|* AST\NullComparisonExpression)*/public function SimpleConditionalExpression(){assert($this->lexer->lookahead !== null);if ($this->lexer->isNextToken(TokenType::T_EXISTS)) {return $this->ExistsExpression();}$token = $this->lexer->lookahead;$peek = $this->lexer->glimpse();$lookahead = $token;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$token = $this->lexer->glimpse();}assert($token !== null);assert($peek !== null);if ($token->type === TokenType::T_IDENTIFIER || $token->type === TokenType::T_INPUT_PARAMETER || $this->isFunction()) {// Peek beyond the matching closing parenthesis.$beyond = $this->lexer->peek();switch ($peek->value) {case '(':// Peeks beyond the matched closing parenthesis.$token = $this->peekBeyondClosingParenthesis(false);assert($token !== null);if ($token->type === TokenType::T_NOT) {$token = $this->lexer->peek();assert($token !== null);}if ($token->type === TokenType::T_IS) {$lookahead = $this->lexer->peek();}break;default:// Peek beyond the PathExpression or InputParameter.$token = $beyond;while ($token->value === '.') {$this->lexer->peek();$token = $this->lexer->peek();assert($token !== null);}// Also peek beyond a NOT if there is one.assert($token !== null);if ($token->type === TokenType::T_NOT) {$token = $this->lexer->peek();assert($token !== null);}// We need to go even further in case of IS (differentiate between NULL and EMPTY)$lookahead = $this->lexer->peek();}assert($lookahead !== null);// Also peek beyond a NOT if there is one.if ($lookahead->type === TokenType::T_NOT) {$lookahead = $this->lexer->peek();}$this->lexer->resetPeek();}if ($token->type === TokenType::T_BETWEEN) {return $this->BetweenExpression();}if ($token->type === TokenType::T_LIKE) {return $this->LikeExpression();}if ($token->type === TokenType::T_IN) {return $this->InExpression();}if ($token->type === TokenType::T_INSTANCE) {return $this->InstanceOfExpression();}if ($token->type === TokenType::T_MEMBER) {return $this->CollectionMemberExpression();}assert($lookahead !== null);if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_NULL) {return $this->NullComparisonExpression();}if ($token->type === TokenType::T_IS && $lookahead->type === TokenType::T_EMPTY) {return $this->EmptyCollectionComparisonExpression();}return $this->ComparisonExpression();}/*** EmptyCollectionComparisonExpression ::= CollectionValuedPathExpression "IS" ["NOT"] "EMPTY"** @return AST\EmptyCollectionComparisonExpression*/public function EmptyCollectionComparisonExpression(){$pathExpression = $this->CollectionValuedPathExpression();$this->match(TokenType::T_IS);$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_EMPTY);return new AST\EmptyCollectionComparisonExpression($pathExpression,$not);}/*** CollectionMemberExpression ::= EntityExpression ["NOT"] "MEMBER" ["OF"] CollectionValuedPathExpression** EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression* SimpleEntityExpression ::= IdentificationVariable | InputParameter** @return AST\CollectionMemberExpression*/public function CollectionMemberExpression(){$not = false;$entityExpr = $this->EntityExpression();if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_MEMBER);if ($this->lexer->isNextToken(TokenType::T_OF)) {$this->match(TokenType::T_OF);}return new AST\CollectionMemberExpression($entityExpr,$this->CollectionValuedPathExpression(),$not);}/*** Literal ::= string | char | integer | float | boolean** @return AST\Literal*/public function Literal(){assert($this->lexer->lookahead !== null);assert($this->lexer->token !== null);switch ($this->lexer->lookahead->type) {case TokenType::T_STRING:$this->match(TokenType::T_STRING);return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);case TokenType::T_INTEGER:case TokenType::T_FLOAT:$this->match($this->lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT);return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token->value);case TokenType::T_TRUE:case TokenType::T_FALSE:$this->match($this->lexer->isNextToken(TokenType::T_TRUE) ? TokenType::T_TRUE : TokenType::T_FALSE);return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token->value);default:$this->syntaxError('Literal');}}/*** InParameter ::= ArithmeticExpression | InputParameter** @return AST\InputParameter|AST\ArithmeticExpression*/public function InParameter(){assert($this->lexer->lookahead !== null);if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) {return $this->InputParameter();}return $this->ArithmeticExpression();}/*** InputParameter ::= PositionalParameter | NamedParameter** @return AST\InputParameter*/public function InputParameter(){$this->match(TokenType::T_INPUT_PARAMETER);assert($this->lexer->token !== null);return new AST\InputParameter($this->lexer->token->value);}/*** ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")"** @return AST\ArithmeticExpression*/public function ArithmeticExpression(){$expr = new AST\ArithmeticExpression();if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {$peek = $this->lexer->glimpse();assert($peek !== null);if ($peek->type === TokenType::T_SELECT) {$this->match(TokenType::T_OPEN_PARENTHESIS);$expr->subselect = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);return $expr;}}$expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression();return $expr;}/*** SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}*** @return AST\SimpleArithmeticExpression|AST\ArithmeticTerm*/public function SimpleArithmeticExpression(){$terms = [];$terms[] = $this->ArithmeticTerm();while (($isPlus = $this->lexer->isNextToken(TokenType::T_PLUS)) || $this->lexer->isNextToken(TokenType::T_MINUS)) {$this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);assert($this->lexer->token !== null);$terms[] = $this->lexer->token->value;$terms[] = $this->ArithmeticTerm();}// Phase 1 AST optimization: Prevent AST\SimpleArithmeticExpression// if only one AST\ArithmeticTerm is definedif (count($terms) === 1) {return $terms[0];}return new AST\SimpleArithmeticExpression($terms);}/*** ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}*** @return AST\ArithmeticTerm*/public function ArithmeticTerm(){$factors = [];$factors[] = $this->ArithmeticFactor();while (($isMult = $this->lexer->isNextToken(TokenType::T_MULTIPLY)) || $this->lexer->isNextToken(TokenType::T_DIVIDE)) {$this->match($isMult ? TokenType::T_MULTIPLY : TokenType::T_DIVIDE);assert($this->lexer->token !== null);$factors[] = $this->lexer->token->value;$factors[] = $this->ArithmeticFactor();}// Phase 1 AST optimization: Prevent AST\ArithmeticTerm// if only one AST\ArithmeticFactor is definedif (count($factors) === 1) {return $factors[0];}return new AST\ArithmeticTerm($factors);}/*** ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary** @return AST\ArithmeticFactor*/public function ArithmeticFactor(){$sign = null;$isPlus = $this->lexer->isNextToken(TokenType::T_PLUS);if ($isPlus || $this->lexer->isNextToken(TokenType::T_MINUS)) {$this->match($isPlus ? TokenType::T_PLUS : TokenType::T_MINUS);$sign = $isPlus;}$primary = $this->ArithmeticPrimary();// Phase 1 AST optimization: Prevent AST\ArithmeticFactor// if only one AST\ArithmeticPrimary is definedif ($sign === null) {return $primary;}return new AST\ArithmeticFactor($primary, $sign);}/*** ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression* | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings* | FunctionsReturningDatetime | IdentificationVariable | ResultVariable* | InputParameter | CaseExpression** @return AST\Node|string*/public function ArithmeticPrimary(){if ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)) {$this->match(TokenType::T_OPEN_PARENTHESIS);$expr = $this->SimpleArithmeticExpression();$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\ParenthesisExpression($expr);}if ($this->lexer->lookahead === null) {$this->syntaxError('ArithmeticPrimary');}switch ($this->lexer->lookahead->type) {case TokenType::T_COALESCE:case TokenType::T_NULLIF:case TokenType::T_CASE:return $this->CaseExpression();case TokenType::T_IDENTIFIER:$peek = $this->lexer->glimpse();if ($peek !== null && $peek->value === '(') {return $this->FunctionDeclaration();}if ($peek !== null && $peek->value === '.') {return $this->SingleValuedPathExpression();}if (isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) {return $this->ResultVariable();}return $this->StateFieldPathExpression();case TokenType::T_INPUT_PARAMETER:return $this->InputParameter();default:$peek = $this->lexer->glimpse();if ($peek !== null && $peek->value === '(') {return $this->FunctionDeclaration();}return $this->Literal();}}/*** StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"** @return AST\Subselect|AST\Node|string*/public function StringExpression(){$peek = $this->lexer->glimpse();assert($peek !== null);// Subselectif ($this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS) && $peek->type === TokenType::T_SELECT) {$this->match(TokenType::T_OPEN_PARENTHESIS);$expr = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);return $expr;}assert($this->lexer->lookahead !== null);// ResultVariable (string)if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) &&isset($this->queryComponents[$this->lexer->lookahead->value]['resultVariable'])) {return $this->ResultVariable();}return $this->StringPrimary();}/*** StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression** @return AST\Node*/public function StringPrimary(){assert($this->lexer->lookahead !== null);$lookaheadType = $this->lexer->lookahead->type;switch ($lookaheadType) {case TokenType::T_IDENTIFIER:$peek = $this->lexer->glimpse();assert($peek !== null);if ($peek->value === '.') {return $this->StateFieldPathExpression();}if ($peek->value === '(') {// do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions.return $this->FunctionDeclaration();}$this->syntaxError("'.' or '('");break;case TokenType::T_STRING:$this->match(TokenType::T_STRING);assert($this->lexer->token !== null);return new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);case TokenType::T_INPUT_PARAMETER:return $this->InputParameter();case TokenType::T_CASE:case TokenType::T_COALESCE:case TokenType::T_NULLIF:return $this->CaseExpression();default:assert($lookaheadType !== null);if ($this->isAggregateFunction($lookaheadType)) {return $this->AggregateExpression();}}$this->syntaxError('StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression');}/*** EntityExpression ::= SingleValuedAssociationPathExpression | SimpleEntityExpression** @return AST\InputParameter|AST\PathExpression*/public function EntityExpression(){$glimpse = $this->lexer->glimpse();assert($glimpse !== null);if ($this->lexer->isNextToken(TokenType::T_IDENTIFIER) && $glimpse->value === '.') {return $this->SingleValuedAssociationPathExpression();}return $this->SimpleEntityExpression();}/*** SimpleEntityExpression ::= IdentificationVariable | InputParameter** @return AST\InputParameter|AST\PathExpression*/public function SimpleEntityExpression(){if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {return $this->InputParameter();}return $this->StateFieldPathExpression();}/*** AggregateExpression ::=* ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")"** @return AST\AggregateExpression*/public function AggregateExpression(){assert($this->lexer->lookahead !== null);$lookaheadType = $this->lexer->lookahead->type;$isDistinct = false;if (! in_array($lookaheadType, [TokenType::T_COUNT, TokenType::T_AVG, TokenType::T_MAX, TokenType::T_MIN, TokenType::T_SUM], true)) {$this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT');}$this->match($lookaheadType);assert($this->lexer->token !== null);$functionName = $this->lexer->token->value;$this->match(TokenType::T_OPEN_PARENTHESIS);if ($this->lexer->isNextToken(TokenType::T_DISTINCT)) {$this->match(TokenType::T_DISTINCT);$isDistinct = true;}$pathExp = $this->SimpleArithmeticExpression();$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\AggregateExpression($functionName, $pathExp, $isDistinct);}/*** QuantifiedExpression ::= ("ALL" | "ANY" | "SOME") "(" Subselect ")"** @return AST\QuantifiedExpression*/public function QuantifiedExpression(){assert($this->lexer->lookahead !== null);$lookaheadType = $this->lexer->lookahead->type;$value = $this->lexer->lookahead->value;if (! in_array($lookaheadType, [TokenType::T_ALL, TokenType::T_ANY, TokenType::T_SOME], true)) {$this->syntaxError('ALL, ANY or SOME');}$this->match($lookaheadType);$this->match(TokenType::T_OPEN_PARENTHESIS);$qExpr = new AST\QuantifiedExpression($this->Subselect());$qExpr->type = $value;$this->match(TokenType::T_CLOSE_PARENTHESIS);return $qExpr;}/*** BetweenExpression ::= ArithmeticExpression ["NOT"] "BETWEEN" ArithmeticExpression "AND" ArithmeticExpression** @return AST\BetweenExpression*/public function BetweenExpression(){$not = false;$arithExpr1 = $this->ArithmeticExpression();if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_BETWEEN);$arithExpr2 = $this->ArithmeticExpression();$this->match(TokenType::T_AND);$arithExpr3 = $this->ArithmeticExpression();return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not);}/*** ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression )** @return AST\ComparisonExpression*/public function ComparisonExpression(){$this->lexer->glimpse();$leftExpr = $this->ArithmeticExpression();$operator = $this->ComparisonOperator();$rightExpr = $this->isNextAllAnySome()? $this->QuantifiedExpression(): $this->ArithmeticExpression();return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr);}/*** InExpression ::= SingleValuedPathExpression ["NOT"] "IN" "(" (InParameter {"," InParameter}* | Subselect) ")"** @return AST\InListExpression|AST\InSubselectExpression*/public function InExpression(){$expression = $this->ArithmeticExpression();$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_IN);$this->match(TokenType::T_OPEN_PARENTHESIS);if ($this->lexer->isNextToken(TokenType::T_SELECT)) {$inExpression = new AST\InSubselectExpression($expression,$this->Subselect(),$not);} else {$literals = [$this->InParameter()];while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$literals[] = $this->InParameter();}$inExpression = new AST\InListExpression($expression,$literals,$not);}$this->match(TokenType::T_CLOSE_PARENTHESIS);return $inExpression;}/*** InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")")** @return AST\InstanceOfExpression*/public function InstanceOfExpression(){$identificationVariable = $this->IdentificationVariable();$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_INSTANCE);$this->match(TokenType::T_OF);$exprValues = $this->lexer->isNextToken(TokenType::T_OPEN_PARENTHESIS)? $this->InstanceOfParameterList(): [$this->InstanceOfParameter()];return new AST\InstanceOfExpression($identificationVariable,$exprValues,$not);}/** @return non-empty-list<AST\InputParameter|string> */public function InstanceOfParameterList(): array{$this->match(TokenType::T_OPEN_PARENTHESIS);$exprValues = [$this->InstanceOfParameter()];while ($this->lexer->isNextToken(TokenType::T_COMMA)) {$this->match(TokenType::T_COMMA);$exprValues[] = $this->InstanceOfParameter();}$this->match(TokenType::T_CLOSE_PARENTHESIS);return $exprValues;}/*** InstanceOfParameter ::= AbstractSchemaName | InputParameter** @return AST\InputParameter|string*/public function InstanceOfParameter(){if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {$this->match(TokenType::T_INPUT_PARAMETER);assert($this->lexer->token !== null);return new AST\InputParameter($this->lexer->token->value);}$abstractSchemaName = $this->AbstractSchemaName();$this->validateAbstractSchemaName($abstractSchemaName);return $abstractSchemaName;}/*** LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char]** @return AST\LikeExpression*/public function LikeExpression(){$stringExpr = $this->StringExpression();$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_LIKE);if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) {$this->match(TokenType::T_INPUT_PARAMETER);assert($this->lexer->token !== null);$stringPattern = new AST\InputParameter($this->lexer->token->value);} else {$stringPattern = $this->StringPrimary();}$escapeChar = null;if ($this->lexer->lookahead !== null && $this->lexer->lookahead->type === TokenType::T_ESCAPE) {$this->match(TokenType::T_ESCAPE);$this->match(TokenType::T_STRING);assert($this->lexer->token !== null);$escapeChar = new AST\Literal(AST\Literal::STRING, $this->lexer->token->value);}return new AST\LikeExpression($stringExpr, $stringPattern, $escapeChar, $not);}/*** NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | AggregateExpression | FunctionDeclaration | IdentificationVariable | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL"** @return AST\NullComparisonExpression*/public function NullComparisonExpression(){switch (true) {case $this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER):$this->match(TokenType::T_INPUT_PARAMETER);assert($this->lexer->token !== null);$expr = new AST\InputParameter($this->lexer->token->value);break;case $this->lexer->isNextToken(TokenType::T_NULLIF):$expr = $this->NullIfExpression();break;case $this->lexer->isNextToken(TokenType::T_COALESCE):$expr = $this->CoalesceExpression();break;case $this->isFunction():$expr = $this->FunctionDeclaration();break;default:// We need to check if we are in a IdentificationVariable or SingleValuedPathExpression$glimpse = $this->lexer->glimpse();assert($glimpse !== null);if ($glimpse->type === TokenType::T_DOT) {$expr = $this->SingleValuedPathExpression();// Leave switch statementbreak;}assert($this->lexer->lookahead !== null);$lookaheadValue = $this->lexer->lookahead->value;// Validate existing componentif (! isset($this->queryComponents[$lookaheadValue])) {$this->semanticalError('Cannot add having condition on undefined result variable.');}// Validate SingleValuedPathExpression (ie.: "product")if (isset($this->queryComponents[$lookaheadValue]['metadata'])) {$expr = $this->SingleValuedPathExpression();break;}// Validating ResultVariableif (! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) {$this->semanticalError('Cannot add having condition on a non result variable.');}$expr = $this->ResultVariable();break;}$this->match(TokenType::T_IS);$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_NULL);return new AST\NullComparisonExpression($expr, $not);}/*** ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")"** @return AST\ExistsExpression*/public function ExistsExpression(){$not = false;if ($this->lexer->isNextToken(TokenType::T_NOT)) {$this->match(TokenType::T_NOT);$not = true;}$this->match(TokenType::T_EXISTS);$this->match(TokenType::T_OPEN_PARENTHESIS);$subselect = $this->Subselect();$this->match(TokenType::T_CLOSE_PARENTHESIS);return new AST\ExistsExpression($subselect, $not);}/*** ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!="** @return string*/public function ComparisonOperator(){assert($this->lexer->lookahead !== null);switch ($this->lexer->lookahead->value) {case '=':$this->match(TokenType::T_EQUALS);return '=';case '<':$this->match(TokenType::T_LOWER_THAN);$operator = '<';if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {$this->match(TokenType::T_EQUALS);$operator .= '=';} elseif ($this->lexer->isNextToken(TokenType::T_GREATER_THAN)) {$this->match(TokenType::T_GREATER_THAN);$operator .= '>';}return $operator;case '>':$this->match(TokenType::T_GREATER_THAN);$operator = '>';if ($this->lexer->isNextToken(TokenType::T_EQUALS)) {$this->match(TokenType::T_EQUALS);$operator .= '=';}return $operator;case '!':$this->match(TokenType::T_NEGATE);$this->match(TokenType::T_EQUALS);return '<>';default:$this->syntaxError('=, <, <=, <>, >, >=, !=');}}/*** FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime** @return Functions\FunctionNode*/public function FunctionDeclaration(){assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$funcName = strtolower($token->value);$customFunctionDeclaration = $this->CustomFunctionDeclaration();// Check for custom functions functions first!switch (true) {case $customFunctionDeclaration !== null:return $customFunctionDeclaration;case isset(self::$stringFunctions[$funcName]):return $this->FunctionsReturningStrings();case isset(self::$numericFunctions[$funcName]):return $this->FunctionsReturningNumerics();case isset(self::$datetimeFunctions[$funcName]):return $this->FunctionsReturningDatetime();default:$this->syntaxError('known function', $token);}}/*** Helper function for FunctionDeclaration grammar rule.*/private function CustomFunctionDeclaration(): ?Functions\FunctionNode{assert($this->lexer->lookahead !== null);$token = $this->lexer->lookahead;$funcName = strtolower($token->value);// Check for custom functions afterwards$config = $this->em->getConfiguration();switch (true) {case $config->getCustomStringFunction($funcName) !== null:return $this->CustomFunctionsReturningStrings();case $config->getCustomNumericFunction($funcName) !== null:return $this->CustomFunctionsReturningNumerics();case $config->getCustomDatetimeFunction($funcName) !== null:return $this->CustomFunctionsReturningDatetime();default:return null;}}/*** FunctionsReturningNumerics ::=* "LENGTH" "(" StringPrimary ")" |* "LOCATE" "(" StringPrimary "," StringPrimary ["," SimpleArithmeticExpression]")" |* "ABS" "(" SimpleArithmeticExpression ")" |* "SQRT" "(" SimpleArithmeticExpression ")" |* "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |* "SIZE" "(" CollectionValuedPathExpression ")" |* "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |* "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |* "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"** @return Functions\FunctionNode*/public function FunctionsReturningNumerics(){assert($this->lexer->lookahead !== null);$funcNameLower = strtolower($this->lexer->lookahead->value);$funcClass = self::$numericFunctions[$funcNameLower];$function = new $funcClass($funcNameLower);$function->parse($this);return $function;}/** @return Functions\FunctionNode */public function CustomFunctionsReturningNumerics(){assert($this->lexer->lookahead !== null);// getCustomNumericFunction is case-insensitive$functionName = strtolower($this->lexer->lookahead->value);$functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName);assert($functionClass !== null);$function = is_string($functionClass)? new $functionClass($functionName): $functionClass($functionName);$function->parse($this);return $function;}/*** FunctionsReturningDateTime ::=* "CURRENT_DATE" |* "CURRENT_TIME" |* "CURRENT_TIMESTAMP" |* "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" |* "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")"** @return Functions\FunctionNode*/public function FunctionsReturningDatetime(){assert($this->lexer->lookahead !== null);$funcNameLower = strtolower($this->lexer->lookahead->value);$funcClass = self::$datetimeFunctions[$funcNameLower];$function = new $funcClass($funcNameLower);$function->parse($this);return $function;}/** @return Functions\FunctionNode */public function CustomFunctionsReturningDatetime(){assert($this->lexer->lookahead !== null);// getCustomDatetimeFunction is case-insensitive$functionName = $this->lexer->lookahead->value;$functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName);assert($functionClass !== null);$function = is_string($functionClass)? new $functionClass($functionName): $functionClass($functionName);$function->parse($this);return $function;}/*** FunctionsReturningStrings ::=* "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary}* ")" |* "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" |* "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" |* "LOWER" "(" StringPrimary ")" |* "UPPER" "(" StringPrimary ")" |* "IDENTITY" "(" SingleValuedAssociationPathExpression {"," string} ")"** @return Functions\FunctionNode*/public function FunctionsReturningStrings(){assert($this->lexer->lookahead !== null);$funcNameLower = strtolower($this->lexer->lookahead->value);$funcClass = self::$stringFunctions[$funcNameLower];$function = new $funcClass($funcNameLower);$function->parse($this);return $function;}/** @return Functions\FunctionNode */public function CustomFunctionsReturningStrings(){assert($this->lexer->lookahead !== null);// getCustomStringFunction is case-insensitive$functionName = $this->lexer->lookahead->value;$functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName);assert($functionClass !== null);$function = is_string($functionClass)? new $functionClass($functionName): $functionClass($functionName);$function->parse($this);return $function;}private function getMetadataForDqlAlias(string $dqlAlias): ClassMetadata{if (! isset($this->queryComponents[$dqlAlias]['metadata'])) {throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias));}return $this->queryComponents[$dqlAlias]['metadata'];}}