From 05e38eadbeb3de19e5b26003c2b401e00bb71df4 Mon Sep 17 00:00:00 2001 From: polo Date: Sun, 18 Oct 2026 00:08:23 +0200 Subject: =?UTF-8?q?sortie=20de=20vendor=20du=20d=C3=A9p=C3=B4t,=20oubli=20?= =?UTF-8?q?dans=20config-template.ini?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vendor/doctrine/orm/src/Query/Parser.php | 3269 ------------------------------ 1 file changed, 3269 deletions(-) delete mode 100644 vendor/doctrine/orm/src/Query/Parser.php (limited to 'vendor/doctrine/orm/src/Query/Parser.php') diff --git a/vendor/doctrine/orm/src/Query/Parser.php b/vendor/doctrine/orm/src/Query/Parser.php deleted file mode 100644 index e948f2c..0000000 --- a/vendor/doctrine/orm/src/Query/Parser.php +++ /dev/null @@ -1,3269 +0,0 @@ - - * @psalm-type QueryComponent = array{ - * metadata?: ClassMetadata, - * parent?: string|null, - * relation?: AssociationMapping|null, - * map?: string|null, - * resultVariable?: AST\Node|string, - * nestingLevel: int, - * token: DqlToken, - * } - */ -final class Parser -{ - /** - * @readonly Maps BUILT-IN string function names to AST class names. - * @psalm-var array> - */ - private static array $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> - */ - private static array $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> - */ - private static array $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 */ - private array $deferredIdentificationVariables = []; - - /** @psalm-var list */ - private array $deferredPathExpressions = []; - - /** @psalm-var list */ - private array $deferredResultVariables = []; - - /** @psalm-var list */ - private array $deferredNewObjectExpressions = []; - - /** - * The lexer. - */ - private readonly Lexer $lexer; - - /** - * The parser result. - */ - private readonly ParserResult $parserResult; - - /** - * The EntityManager. - */ - private readonly EntityManagerInterface $em; - - /** - * Map of declared query components in the parsed query. - * - * @psalm-var array - */ - private array $queryComponents = []; - - /** - * Keeps the nesting level of defined ResultVariables. - */ - private int $nestingLevel = 0; - - /** - * Any additional custom tree walkers that modify the AST. - * - * @psalm-var list> - */ - private array $customTreeWalkers = []; - - /** - * The custom last tree walker, if any, that is responsible for producing the output. - * - * @var class-string|null - */ - private $customOutputWalker; - - /** @psalm-var array */ - private array $identVariableExpressions = []; - - /** - * Creates a new query parser object. - * - * @param Query $query The Query to parse. - */ - public function __construct(private readonly 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. - * - * @psalm-param class-string $className - */ - public function setCustomOutputTreeWalker(string $className): void - { - $this->customOutputWalker = $className; - } - - /** - * Adds a custom tree walker for modifying the AST. - * - * @psalm-param class-string $className - */ - public function addCustomTreeWalker(string $className): void - { - $this->customTreeWalkers[] = $className; - } - - /** - * Gets the lexer used by the parser. - */ - public function getLexer(): Lexer - { - return $this->lexer; - } - - /** - * Gets the ParserResult that is being filled with information during parsing. - */ - public function getParserResult(): ParserResult - { - return $this->parserResult; - } - - /** - * Gets the EntityManager used by the parser. - */ - public function getEntityManager(): EntityManagerInterface - { - return $this->em; - } - - /** - * Parses and builds AST for the given Query. - */ - public function getAST(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement - { - // 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->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. - * - * @throws QueryException If the tokens don't match. - */ - public function match(TokenType $token): void - { - $lookaheadType = $this->lexer->lookahead->type ?? null; - - // Short-circuit on first condition, usually types match - if ($lookaheadType === $token) { - $this->lexer->moveNext(); - - return; - } - - // If parameter is not identifier (1-99) must be exact match - if ($token->value < TokenType::T_IDENTIFIER->value) { - $this->syntaxError($this->lexer->getLiteral($token)); - } - - // If parameter is keyword (200+) must be exact match - if ($token->value > TokenType::T_IDENTIFIER->value) { - $this->syntaxError($this->lexer->getLiteral($token)); - } - - // If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+) - if ($token->value === TokenType::T_IDENTIFIER->value && $lookaheadType->value < TokenType::T_IDENTIFIER->value) { - $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. - */ - public function free(bool $deep = false, int $position = 0): void - { - // WARNING! Use this method with care. It resets the scanner! - $this->lexer->resetPosition($position); - - // Deep = true cleans peek and also any previously defined errors - if ($deep) { - $this->lexer->resetPeek(); - } - - $this->lexer->token = null; - $this->lexer->lookahead = null; - } - - /** - * Parses a query string. - */ - public function parse(): ParserResult - { - $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 AST - if ($this->customTreeWalkers) { - $treeWalkerChain = new TreeWalkerChain($this->query, $this->parserResult, $this->queryComponents); - - foreach ($this->customTreeWalkers as $walker) { - $treeWalkerChain->addTreeWalker($walker); - } - - match (true) { - $AST instanceof AST\UpdateStatement => $treeWalkerChain->walkUpdateStatement($AST), - $AST instanceof AST\DeleteStatement => $treeWalkerChain->walkDeleteStatement($AST), - $AST instanceof AST\SelectStatement => $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. - */ - private function fixIdentificationVariableOrder(AST\SelectStatement|AST\DeleteStatement|AST\UpdateStatement $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 DqlToken|null $token Got token. - * - * @throws QueryException - */ - public function syntaxError(string $expected = '', Token|null $token = null): never - { - 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. - * @psalm-param DqlToken|null $token - * - * @throws QueryException - */ - public function semanticalError(string $message = '', Token|null $token = null): never - { - 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. - * - * @psalm-return DqlToken|null - */ - private function peekBeyondClosingParenthesis(bool $resetPeek = true): Token|null - { - $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|null $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->value >= TokenType::T_IDENTIFIER->value && $peek !== null && $peek->type === TokenType::T_OPEN_PARENTHESIS; - } - - /** - * Checks whether the given token type indicates an aggregate function. - * - * @return bool TRUE if the token type is an aggregate function, FALSE otherwise. - */ - private function isAggregateFunction(TokenType $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 IdentificationVariable 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 queryComponents - if (! 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 ResultVariable - if (! 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 one - if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { - $this->semanticalError( - sprintf("'%s' is used outside the scope of its declaration.", $identVariable), - $deferredItem['token'], - ); - } - } - } - - /** - * Validates that the given NewObjectExpression. - */ - 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 namespace - if (! 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 ResultVariable 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 queryComponents - if (! 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 ResultVariable - if (! 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 one - if ($qComp['nestingLevel'] > $deferredItem['nestingLevel']) { - $this->semanticalError( - sprintf("'%s' is used outside the scope of its declaration.", $resultVariable), - $deferredItem['token'], - ); - } - } - } - - /** - * Validates that the given PathExpression 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 exists - if (! 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->isToOne() - ? 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 type - if ($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 - */ - public function QueryLanguage(): AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement - { - $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 string - if ($this->lexer->lookahead !== null) { - $this->syntaxError('end of string'); - } - - return $statement; - } - - /** - * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] - */ - public function SelectStatement(): AST\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] - */ - public function UpdateStatement(): AST\UpdateStatement - { - $updateStatement = new AST\UpdateStatement($this->UpdateClause()); - - $updateStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; - - return $updateStatement; - } - - /** - * DeleteStatement ::= DeleteClause [WhereClause] - */ - public function DeleteStatement(): AST\DeleteStatement - { - $deleteStatement = new AST\DeleteStatement($this->DeleteClause()); - - $deleteStatement->whereClause = $this->lexer->isNextToken(TokenType::T_WHERE) ? $this->WhereClause() : null; - - return $deleteStatement; - } - - /** - * IdentificationVariable ::= identifier - */ - public function IdentificationVariable(): string - { - $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 - */ - public function AliasIdentificationVariable(): string - { - $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 | identifier - */ - public function AbstractSchemaName(): string - { - 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; - } - - $this->match(TokenType::T_IDENTIFIER); - assert($this->lexer->token !== null); - - return $this->lexer->token->value; - } - - /** - * 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 - */ - public function AliasResultVariable(): string - { - $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 - */ - public function ResultVariable(): string - { - $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) - */ - public function JoinAssociationPathExpression(): AST\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}* - * - * @psalm-param int-mask-of $expectedTypes - */ - public function PathExpression(int $expectedTypes): AST\PathExpression - { - $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 - */ - public function AssociationPathExpression(): AST\PathExpression - { - return $this->PathExpression( - AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | - AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION, - ); - } - - /** - * SingleValuedPathExpression ::= StateFieldPathExpression | SingleValuedAssociationPathExpression - */ - public function SingleValuedPathExpression(): AST\PathExpression - { - return $this->PathExpression( - AST\PathExpression::TYPE_STATE_FIELD | - AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, - ); - } - - /** - * StateFieldPathExpression ::= IdentificationVariable "." StateField - */ - public function StateFieldPathExpression(): AST\PathExpression - { - return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); - } - - /** - * SingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField - */ - public function SingleValuedAssociationPathExpression(): AST\PathExpression - { - return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); - } - - /** - * CollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField - */ - public function CollectionValuedPathExpression(): AST\PathExpression - { - return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); - } - - /** - * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} - */ - public function SelectClause(): AST\SelectClause - { - $isDistinct = false; - $this->match(TokenType::T_SELECT); - - // Check for DISTINCT - if ($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 - */ - public function SimpleSelectClause(): AST\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}* - */ - public function UpdateClause(): AST\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 - */ - public function DeleteClause(): AST\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}* - */ - public function FromClause(): AST\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}* - */ - public function SubselectFromClause(): AST\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 - */ - public function WhereClause(): AST\WhereClause - { - $this->match(TokenType::T_WHERE); - - return new AST\WhereClause($this->ConditionalExpression()); - } - - /** - * HavingClause ::= "HAVING" ConditionalExpression - */ - public function HavingClause(): AST\HavingClause - { - $this->match(TokenType::T_HAVING); - - return new AST\HavingClause($this->ConditionalExpression()); - } - - /** - * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* - */ - public function GroupByClause(): AST\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}* - */ - public function OrderByClause(): AST\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] - */ - public function Subselect(): AST\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 - */ - public function UpdateItem(): AST\UpdateItem - { - $pathExpr = $this->SingleValuedPathExpression(); - - $this->match(TokenType::T_EQUALS); - - return new AST\UpdateItem($pathExpr, $this->NewValue()); - } - - /** - * GroupByItem ::= IdentificationVariable | ResultVariable | SingleValuedPathExpression - */ - public function GroupByItem(): string|AST\PathExpression - { - // 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"] - */ - public function OrderByItem(): AST\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); - $expr = match (true) { - $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), - $glimpse !== null && $glimpse->type === TokenType::T_DOT => $this->SingleValuedPathExpression(), - $this->lexer->peek() && $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), - $this->lexer->lookahead->type === TokenType::T_CASE => $this->CaseExpression(), - $this->isFunction() => $this->FunctionDeclaration(), - default => $this->ResultVariable(), - }; - - $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 - */ - public function NewValue(): AST\ArithmeticExpression|AST\InputParameter|null - { - 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}* - */ - public function IdentificationVariableDeclaration(): AST\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.} - */ - public function SubselectIdentificationVariableDeclaration(): AST\IdentificationVariableDeclaration - { - /* - 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] - */ - public function Join(): AST\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 join - if ($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 declaration - if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { - $joinDeclaration->isRoot = false; - } - - // Check for ad-hoc Join conditions - if ($adhocConditions) { - $this->match(TokenType::T_WITH); - - $join->conditionalExpression = $this->ConditionalExpression(); - } - - return $join; - } - - /** - * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable - * - * @throws QueryException - */ - public function RangeVariableDeclaration(): AST\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] - */ - public function JoinAssociationDeclaration(): AST\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); - } - - /** - * NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" - */ - public function NewObjectExpression(): AST\NewObjectExpression - { - $args = []; - $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 ")" - */ - public function NewObjectArg(): mixed - { - 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 - */ - public function IndexBy(): AST\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(): mixed - { - 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: - return match (true) { - $this->isMathOperator($peek) => $this->SimpleArithmeticExpression(), - default => $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 FunctionDeclaration - return $this->CaseExpression(); - - case $lookahead === TokenType::T_OPEN_PARENTHESIS: - return $this->SimpleArithmeticExpression(); - - // this check must be done before checking for a filed path expression - case $this->isFunction(): - $this->lexer->peek(); - - return match (true) { - $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->SimpleArithmeticExpression(), - default => $this->FunctionDeclaration(), - }; - - // it is no function, so it must be a field path - case $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(): mixed - { - 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 nothing - break; - } - - $this->syntaxError(); - } - - /** - * CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")" - */ - public function CoalesceExpression(): AST\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 ")" - */ - public function NullIfExpression(): AST\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" - */ - public function GeneralCaseExpression(): AST\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 - */ - public function SimpleCaseExpression(): AST\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 - */ - public function WhenClause(): AST\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 - */ - public function SimpleWhenClause(): AST\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 | - * "(" Subselect ")" | CaseExpression | NewObjectExpression - * ) [["AS"] ["HIDDEN"] AliasResultVariable] - */ - public function SelectExpression(): AST\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(); // "(" - - $expression = match (true) { - $this->isMathOperator($this->peekBeyondClosingParenthesis()) => $this->ScalarExpression(), - default => $this->FunctionDeclaration(), - }; - - break; - - // Subselect - case $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 => SimpleArithmeticExpression - case $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 | "(" 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] - */ - public function SimpleSelectExpression(): AST\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}* - */ - public function ConditionalExpression(): AST\ConditionalExpression|AST\ConditionalFactor|AST\ConditionalPrimary|AST\ConditionalTerm - { - $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 defined - if (count($conditionalTerms) === 1) { - return $conditionalTerms[0]; - } - - return new AST\ConditionalExpression($conditionalTerms); - } - - /** - * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* - */ - public function ConditionalTerm(): AST\ConditionalFactor|AST\ConditionalPrimary|AST\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 defined - if (count($conditionalFactors) === 1) { - return $conditionalFactors[0]; - } - - return new AST\ConditionalTerm($conditionalFactors); - } - - /** - * ConditionalFactor ::= ["NOT"] ConditionalPrimary - */ - public function ConditionalFactor(): AST\ConditionalFactor|AST\ConditionalPrimary - { - $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 defined - if (! $not) { - return $conditionalPrimary; - } - - return new AST\ConditionalFactor($conditionalPrimary, $not); - } - - /** - * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" - */ - public function ConditionalPrimary(): AST\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 - */ - public function SimpleConditionalExpression(): AST\ExistsExpression|AST\BetweenExpression|AST\LikeExpression|AST\InListExpression|AST\InSubselectExpression|AST\InstanceOfExpression|AST\CollectionMemberExpression|AST\NullComparisonExpression|AST\EmptyCollectionComparisonExpression|AST\ComparisonExpression - { - 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" - */ - public function EmptyCollectionComparisonExpression(): AST\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 - */ - public function CollectionMemberExpression(): AST\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 - */ - public function Literal(): AST\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 - */ - public function InParameter(): AST\InputParameter|AST\ArithmeticExpression - { - assert($this->lexer->lookahead !== null); - if ($this->lexer->lookahead->type === TokenType::T_INPUT_PARAMETER) { - return $this->InputParameter(); - } - - return $this->ArithmeticExpression(); - } - - /** - * InputParameter ::= PositionalParameter | NamedParameter - */ - public function InputParameter(): AST\InputParameter - { - $this->match(TokenType::T_INPUT_PARAMETER); - assert($this->lexer->token !== null); - - return new AST\InputParameter($this->lexer->token->value); - } - - /** - * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" - */ - public function ArithmeticExpression(): AST\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}* - */ - public function SimpleArithmeticExpression(): AST\Node|string - { - $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 defined - if (count($terms) === 1) { - return $terms[0]; - } - - return new AST\SimpleArithmeticExpression($terms); - } - - /** - * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* - */ - public function ArithmeticTerm(): AST\Node|string - { - $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 defined - if (count($factors) === 1) { - return $factors[0]; - } - - return new AST\ArithmeticTerm($factors); - } - - /** - * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary - */ - public function ArithmeticFactor(): AST\Node|string|AST\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 defined - if ($sign === null) { - return $primary; - } - - return new AST\ArithmeticFactor($primary, $sign); - } - - /** - * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression - * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings - * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable - * | InputParameter | CaseExpression - */ - public function ArithmeticPrimary(): AST\Node|string - { - 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 ")" - */ - public function StringExpression(): AST\Subselect|AST\Node|string - { - $peek = $this->lexer->glimpse(); - assert($peek !== null); - - // Subselect - if ($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 - */ - public function StringPrimary(): AST\Node - { - 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 - */ - public function EntityExpression(): AST\InputParameter|AST\PathExpression - { - $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 - */ - public function SimpleEntityExpression(): AST\InputParameter|AST\PathExpression - { - if ($this->lexer->isNextToken(TokenType::T_INPUT_PARAMETER)) { - return $this->InputParameter(); - } - - return $this->StateFieldPathExpression(); - } - - /** - * AggregateExpression ::= - * ("AVG" | "MAX" | "MIN" | "SUM" | "COUNT") "(" ["DISTINCT"] SimpleArithmeticExpression ")" - */ - public function AggregateExpression(): AST\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 ")" - */ - public function QuantifiedExpression(): AST\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 - */ - public function BetweenExpression(): AST\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 ) - */ - public function ComparisonExpression(): AST\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) ")" - */ - public function InExpression(): AST\InListExpression|AST\InSubselectExpression - { - $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}* ")") - */ - public function InstanceOfExpression(): AST\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 */ - 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 - */ - public function InstanceOfParameter(): AST\InputParameter|string - { - 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] - */ - public function LikeExpression(): AST\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" - */ - public function NullComparisonExpression(): AST\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 statement - break; - } - - assert($this->lexer->lookahead !== null); - $lookaheadValue = $this->lexer->lookahead->value; - - // Validate existing component - if (! 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 ResultVariable - if (! 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 ")" - */ - public function ExistsExpression(): AST\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 ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" - */ - public function ComparisonOperator(): string - { - 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 - */ - public function FunctionDeclaration(): Functions\FunctionNode - { - 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|null - { - assert($this->lexer->lookahead !== null); - $token = $this->lexer->lookahead; - $funcName = strtolower($token->value); - - // Check for custom functions afterwards - $config = $this->em->getConfiguration(); - - return match (true) { - $config->getCustomStringFunction($funcName) !== null => $this->CustomFunctionsReturningStrings(), - $config->getCustomNumericFunction($funcName) !== null => $this->CustomFunctionsReturningNumerics(), - $config->getCustomDatetimeFunction($funcName) !== null => $this->CustomFunctionsReturningDatetime(), - default => 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 ")" - */ - public function FunctionsReturningNumerics(): AST\Functions\FunctionNode - { - assert($this->lexer->lookahead !== null); - $funcNameLower = strtolower($this->lexer->lookahead->value); - $funcClass = self::$numericFunctions[$funcNameLower]; - - $function = new $funcClass($funcNameLower); - $function->parse($this); - - return $function; - } - - public function CustomFunctionsReturningNumerics(): AST\Functions\FunctionNode - { - 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 ")" - */ - public function FunctionsReturningDatetime(): AST\Functions\FunctionNode - { - assert($this->lexer->lookahead !== null); - $funcNameLower = strtolower($this->lexer->lookahead->value); - $funcClass = self::$datetimeFunctions[$funcNameLower]; - - $function = new $funcClass($funcNameLower); - $function->parse($this); - - return $function; - } - - public function CustomFunctionsReturningDatetime(): AST\Functions\FunctionNode - { - 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} ")" - */ - public function FunctionsReturningStrings(): AST\Functions\FunctionNode - { - assert($this->lexer->lookahead !== null); - $funcNameLower = strtolower($this->lexer->lookahead->value); - $funcClass = self::$stringFunctions[$funcNameLower]; - - $function = new $funcClass($funcNameLower); - $function->parse($this); - - return $function; - } - - public function CustomFunctionsReturningStrings(): Functions\FunctionNode - { - 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']; - } -} -- cgit v1.2.3