$scalarExpressions = []; $scalarExpressions[] = $this->ScalarExpression(); while ($this->lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); $scalarExpressions[] = $this->ScalarExpression(); } $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\CoalesceExpression($scalarExpressions); } public function NullIfExpression() { $this->match(Lexer::T_NULLIF); $this->match(Lexer::T_OPEN_PARENTHESIS); $firstExpression = $this->ScalarExpression(); $this->match(Lexer::T_COMMA); $secondExpression = $this->ScalarExpression(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\NullIfExpression($firstExpression, $secondExpression); } public function GeneralCaseExpression() { $this->match(Lexer::T_CASE); // Process WhenClause (1..N) $whenClauses = []; do { $whenClauses[] = $this->WhenClause(); } while ($this->lexer->isNextToken(Lexer::T_WHEN)); $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); return new AST\GeneralCaseExpression($whenClauses, $scalarExpression); } public function SimpleCaseExpression() { $this->match(Lexer::T_CASE); $caseOperand = $this->StateFieldPathExpression(); // Process SimpleWhenClause (1..N) $simpleWhenClauses = []; do { $simpleWhenClauses[] = $this->SimpleWhenClause(); } while ($this->lexer->isNextToken(Lexer::T_WHEN)); $this->match(Lexer::T_ELSE); $scalarExpression = $this->ScalarExpression(); $this->match(Lexer::T_END); return new AST\SimpleCaseExpression($caseOperand, $simpleWhenClauses, $scalarExpression); } public function WhenClause() { $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ConditionalExpression(); $this->match(Lexer::T_THEN); return new AST\WhenClause($conditionalExpression, $this->ScalarExpression()); } public function SimpleWhenClause() { $this->match(Lexer::T_WHEN); $conditionalExpression = $this->ScalarExpression(); $this->match(Lexer::T_THEN); return new AST\SimpleWhenClause($conditionalExpression, $this->ScalarExpression()); } public function SelectExpression() { assert($this->lexer->lookahead !== null); $expression = null; $identVariable = null; $peek = $this->lexer->glimpse(); $lookaheadType = $this->lexer->lookahead['type']; assert($peek !== null); switch (\true) { // ScalarExpression (u.name) case $lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] === Lexer::T_DOT: $expression = $this->ScalarExpression(); break; // IdentificationVariable (u) case $lookaheadType === Lexer::T_IDENTIFIER && $peek['type'] !== Lexer::T_OPEN_PARENTHESIS: $expression = $identVariable = $this->IdentificationVariable(); break; // CaseExpression (CASE ... or NULLIF(...) or COALESCE(...)) case $lookaheadType === Lexer::T_CASE: case $lookaheadType === Lexer::T_COALESCE: case $lookaheadType === Lexer::T_NULLIF: $expression = $this->CaseExpression(); break; // DQL Function (SUM(u.value) or SUM(u.value) + 1) case $this->isFunction(): $this->lexer->peek(); // "(" switch (\true) { case $this->isMathOperator($this->peekBeyondClosingParenthesis()): // SUM(u.id) + COUNT(u.id) $expression = $this->ScalarExpression(); break; default: // IDENTITY(u) $expression = $this->FunctionDeclaration(); break; } break; // PartialObjectExpression (PARTIAL u.{id, name}) case $lookaheadType === Lexer::T_PARTIAL: $expression = $this->PartialObjectExpression(); $identVariable = $expression->identificationVariable; break; // Subselect case $lookaheadType === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT: $this->match(Lexer::T_OPEN_PARENTHESIS); $expression = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); break; // Shortcut: ScalarExpression => SimpleArithmeticExpression case $lookaheadType === Lexer::T_OPEN_PARENTHESIS: case $lookaheadType === Lexer::T_INTEGER: case $lookaheadType === Lexer::T_STRING: case $lookaheadType === Lexer::T_FLOAT: // SimpleArithmeticExpression : (- u.value ) or ( + u.value ) case $lookaheadType === Lexer::T_MINUS: case $lookaheadType === Lexer::T_PLUS: $expression = $this->SimpleArithmeticExpression(); break; // NewObjectExpression (New ClassName(id, name)) case $lookaheadType === Lexer::T_NEW: $expression = $this->NewObjectExpression(); break; default: $this->syntaxError('IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression', $this->lexer->lookahead); } // [["AS"] ["HIDDEN"] AliasResultVariable] $mustHaveAliasResultVariable = \false; if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); $mustHaveAliasResultVariable = \true; } $hiddenAliasResultVariable = \false; if ($this->lexer->isNextToken(Lexer::T_HIDDEN)) { $this->match(Lexer::T_HIDDEN); $hiddenAliasResultVariable = \true; } $aliasResultVariable = null; if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(Lexer::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; } public function SimpleSelectExpression() { assert($this->lexer->lookahead !== null); $peek = $this->lexer->glimpse(); assert($peek !== null); switch ($this->lexer->lookahead['type']) { case Lexer::T_IDENTIFIER: switch (\true) { case $peek['type'] === Lexer::T_DOT: $expression = $this->StateFieldPathExpression(); return new AST\SimpleSelectExpression($expression); case $peek['type'] !== Lexer::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: } break; case Lexer::T_OPEN_PARENTHESIS: if ($peek['type'] !== Lexer::T_SELECT) { // Shortcut: ScalarExpression => SimpleArithmeticExpression $expression = $this->SimpleArithmeticExpression(); return new AST\SimpleSelectExpression($expression); } // Subselect $this->match(Lexer::T_OPEN_PARENTHESIS); $expression = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\SimpleSelectExpression($expression); default: } $this->lexer->peek(); $expression = $this->ScalarExpression(); $expr = new AST\SimpleSelectExpression($expression); if ($this->lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS); } if ($this->lexer->isNextToken(Lexer::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; } public function ConditionalExpression() { $conditionalTerms = []; $conditionalTerms[] = $this->ConditionalTerm(); while ($this->lexer->isNextToken(Lexer::T_OR)) { $this->match(Lexer::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); } public function ConditionalTerm() { $conditionalFactors = []; $conditionalFactors[] = $this->ConditionalFactor(); while ($this->lexer->isNextToken(Lexer::T_AND)) { $this->match(Lexer::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); } public function ConditionalFactor() { $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::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); } public function ConditionalPrimary() { $condPrimary = new AST\ConditionalPrimary(); if (!$this->lexer->isNextToken(Lexer::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'], [Lexer::T_NOT, Lexer::T_BETWEEN, Lexer::T_LIKE, Lexer::T_IN, Lexer::T_IS, Lexer::T_EXISTS], \true) || $this->isMathOperator($peek))) { $condPrimary->simpleConditionalExpression = $this->SimpleConditionalExpression(); return $condPrimary; } $this->match(Lexer::T_OPEN_PARENTHESIS); $condPrimary->conditionalExpression = $this->ConditionalExpression(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return $condPrimary; } public function SimpleConditionalExpression() { assert($this->lexer->lookahead !== null); if ($this->lexer->isNextToken(Lexer::T_EXISTS)) { return $this->ExistsExpression(); } $token = $this->lexer->lookahead; $peek = $this->lexer->glimpse(); $lookahead = $token; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $token = $this->lexer->glimpse(); } assert($token !== null); assert($peek !== null); if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::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'] === Lexer::T_NOT) { $token = $this->lexer->peek(); assert($token !== null); } if ($token['type'] === Lexer::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'] === Lexer::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'] === Lexer::T_NOT) { $lookahead = $this->lexer->peek(); } $this->lexer->resetPeek(); } if ($token['type'] === Lexer::T_BETWEEN) { return $this->BetweenExpression(); } if ($token['type'] === Lexer::T_LIKE) { return $this->LikeExpression(); } if ($token['type'] === Lexer::T_IN) { return $this->InExpression(); } if ($token['type'] === Lexer::T_INSTANCE) { return $this->InstanceOfExpression(); } if ($token['type'] === Lexer::T_MEMBER) { return $this->CollectionMemberExpression(); } assert($lookahead !== null); if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_NULL) { return $this->NullComparisonExpression(); } if ($token['type'] === Lexer::T_IS && $lookahead['type'] === Lexer::T_EMPTY) { return $this->EmptyCollectionComparisonExpression(); } return $this->ComparisonExpression(); } public function EmptyCollectionComparisonExpression() { $pathExpression = $this->CollectionValuedPathExpression(); $this->match(Lexer::T_IS); $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_EMPTY); return new AST\EmptyCollectionComparisonExpression($pathExpression, $not); } public function CollectionMemberExpression() { $not = \false; $entityExpr = $this->EntityExpression(); if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_MEMBER); if ($this->lexer->isNextToken(Lexer::T_OF)) { $this->match(Lexer::T_OF); } return new AST\CollectionMemberExpression($entityExpr, $this->CollectionValuedPathExpression(), $not); } public function Literal() { assert($this->lexer->lookahead !== null); assert($this->lexer->token !== null); switch ($this->lexer->lookahead['type']) { case Lexer::T_STRING: $this->match(Lexer::T_STRING); return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']); case Lexer::T_INTEGER: case Lexer::T_FLOAT: $this->match($this->lexer->isNextToken(Lexer::T_INTEGER) ? Lexer::T_INTEGER : Lexer::T_FLOAT); return new AST\Literal(AST\Literal::NUMERIC, $this->lexer->token['value']); case Lexer::T_TRUE: case Lexer::T_FALSE: $this->match($this->lexer->isNextToken(Lexer::T_TRUE) ? Lexer::T_TRUE : Lexer::T_FALSE); return new AST\Literal(AST\Literal::BOOLEAN, $this->lexer->token['value']); default: $this->syntaxError('Literal'); } } public function InParameter() { assert($this->lexer->lookahead !== null); if ($this->lexer->lookahead['type'] === Lexer::T_INPUT_PARAMETER) { return $this->InputParameter(); } return $this->ArithmeticExpression(); } public function InputParameter() { $this->match(Lexer::T_INPUT_PARAMETER); assert($this->lexer->token !== null); return new AST\InputParameter($this->lexer->token['value']); } public function ArithmeticExpression() { $expr = new AST\ArithmeticExpression(); if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { $peek = $this->lexer->glimpse(); assert($peek !== null); if ($peek['type'] === Lexer::T_SELECT) { $this->match(Lexer::T_OPEN_PARENTHESIS); $expr->subselect = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return $expr; } } $expr->simpleArithmeticExpression = $this->SimpleArithmeticExpression(); return $expr; } public function SimpleArithmeticExpression() { $terms = []; $terms[] = $this->ArithmeticTerm(); while (($isPlus = $this->lexer->isNextToken(Lexer::T_PLUS)) || $this->lexer->isNextToken(Lexer::T_MINUS)) { $this->match($isPlus ? Lexer::T_PLUS : Lexer::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); } public function ArithmeticTerm() { $factors = []; $factors[] = $this->ArithmeticFactor(); while (($isMult = $this->lexer->isNextToken(Lexer::T_MULTIPLY)) || $this->lexer->isNextToken(Lexer::T_DIVIDE)) { $this->match($isMult ? Lexer::T_MULTIPLY : Lexer::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); } public function ArithmeticFactor() { $sign = null; $isPlus = $this->lexer->isNextToken(Lexer::T_PLUS); if ($isPlus || $this->lexer->isNextToken(Lexer::T_MINUS)) { $this->match($isPlus ? Lexer::T_PLUS : Lexer::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); } public function ArithmeticPrimary() { if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { $this->match(Lexer::T_OPEN_PARENTHESIS); $expr = $this->SimpleArithmeticExpression(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\ParenthesisExpression($expr); } assert($this->lexer->lookahead !== null); switch ($this->lexer->lookahead['type']) { case Lexer::T_COALESCE: case Lexer::T_NULLIF: case Lexer::T_CASE: return $this->CaseExpression(); case Lexer::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 Lexer::T_INPUT_PARAMETER: return $this->InputParameter(); default: $peek = $this->lexer->glimpse(); if ($peek !== null && $peek['value'] === '(') { return $this->FunctionDeclaration(); } return $this->Literal(); } } public function StringExpression() { $peek = $this->lexer->glimpse(); assert($peek !== null); // Subselect if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) { $this->match(Lexer::T_OPEN_PARENTHESIS); $expr = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return $expr; } assert($this->lexer->lookahead !== null); // ResultVariable (string) if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) { return $this->ResultVariable(); } return $this->StringPrimary(); } public function StringPrimary() { assert($this->lexer->lookahead !== null); $lookaheadType = $this->lexer->lookahead['type']; switch ($lookaheadType) { case Lexer::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 Lexer::T_STRING: $this->match(Lexer::T_STRING); assert($this->lexer->token !== null); return new AST\Literal(AST\Literal::STRING, $this->lexer->token['value']); case Lexer::T_INPUT_PARAMETER: return $this->InputParameter(); case Lexer::T_CASE: case Lexer::T_COALESCE: case Lexer::T_NULLIF: return $this->CaseExpression(); default: assert($lookaheadType !== null); if ($this->isAggregateFunction($lookaheadType)) { return $this->AggregateExpression(); } } $this->syntaxError('StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'); } public function EntityExpression() { $glimpse = $this->lexer->glimpse(); assert($glimpse !== null); if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && $glimpse['value'] === '.') { return $this->SingleValuedAssociationPathExpression(); } return $this->SimpleEntityExpression(); } public function SimpleEntityExpression() { if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { return $this->InputParameter(); } return $this->StateFieldPathExpression(); } public function AggregateExpression() { assert($this->lexer->lookahead !== null); $lookaheadType = $this->lexer->lookahead['type']; $isDistinct = \false; if (!in_array($lookaheadType, [Lexer::T_COUNT, Lexer::T_AVG, Lexer::T_MAX, Lexer::T_MIN, Lexer::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(Lexer::T_OPEN_PARENTHESIS); if ($this->lexer->isNextToken(Lexer::T_DISTINCT)) { $this->match(Lexer::T_DISTINCT); $isDistinct = \true; } $pathExp = $this->SimpleArithmeticExpression(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); } public function QuantifiedExpression() { assert($this->lexer->lookahead !== null); $lookaheadType = $this->lexer->lookahead['type']; $value = $this->lexer->lookahead['value']; if (!in_array($lookaheadType, [Lexer::T_ALL, Lexer::T_ANY, Lexer::T_SOME], \true)) { $this->syntaxError('ALL, ANY or SOME'); } $this->match($lookaheadType); $this->match(Lexer::T_OPEN_PARENTHESIS); $qExpr = new AST\QuantifiedExpression($this->Subselect()); $qExpr->type = $value; $this->match(Lexer::T_CLOSE_PARENTHESIS); return $qExpr; } public function BetweenExpression() { $not = \false; $arithExpr1 = $this->ArithmeticExpression(); if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_BETWEEN); $arithExpr2 = $this->ArithmeticExpression(); $this->match(Lexer::T_AND); $arithExpr3 = $this->ArithmeticExpression(); return new AST\BetweenExpression($arithExpr1, $arithExpr2, $arithExpr3, $not); } public function ComparisonExpression() { $this->lexer->glimpse(); $leftExpr = $this->ArithmeticExpression(); $operator = $this->ComparisonOperator(); $rightExpr = $this->isNextAllAnySome() ? $this->QuantifiedExpression() : $this->ArithmeticExpression(); return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); } public function InExpression() { $expression = $this->ArithmeticExpression(); $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_IN); $this->match(Lexer::T_OPEN_PARENTHESIS); if ($this->lexer->isNextToken(Lexer::T_SELECT)) { $inExpression = new AST\InSubselectExpression($expression, $this->Subselect(), $not); } else { $literals = [$this->InParameter()]; while ($this->lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); $literals[] = $this->InParameter(); } $inExpression = new AST\InListExpression($expression, $literals, $not); } $this->match(Lexer::T_CLOSE_PARENTHESIS); return $inExpression; } public function InstanceOfExpression() { $identificationVariable = $this->IdentificationVariable(); $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_INSTANCE); $this->match(Lexer::T_OF); $exprValues = $this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) ? $this->InstanceOfParameterList() : [$this->InstanceOfParameter()]; return new AST\InstanceOfExpression($identificationVariable, $exprValues, $not); } public function InstanceOfParameterList() : array { $this->match(Lexer::T_OPEN_PARENTHESIS); $exprValues = [$this->InstanceOfParameter()]; while ($this->lexer->isNextToken(Lexer::T_COMMA)) { $this->match(Lexer::T_COMMA); $exprValues[] = $this->InstanceOfParameter(); } $this->match(Lexer::T_CLOSE_PARENTHESIS); return $exprValues; } public function InstanceOfParameter() { if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::T_INPUT_PARAMETER); assert($this->lexer->token !== null); return new AST\InputParameter($this->lexer->token['value']); } $abstractSchemaName = $this->AbstractSchemaName(); $this->validateAbstractSchemaName($abstractSchemaName); return $abstractSchemaName; } public function LikeExpression() { $stringExpr = $this->StringExpression(); $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_LIKE); if ($this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER)) { $this->match(Lexer::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'] === Lexer::T_ESCAPE) { $this->match(Lexer::T_ESCAPE); $this->match(Lexer::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); } public function NullComparisonExpression() { switch (\true) { case $this->lexer->isNextToken(Lexer::T_INPUT_PARAMETER): $this->match(Lexer::T_INPUT_PARAMETER); assert($this->lexer->token !== null); $expr = new AST\InputParameter($this->lexer->token['value']); break; case $this->lexer->isNextToken(Lexer::T_NULLIF): $expr = $this->NullIfExpression(); break; case $this->lexer->isNextToken(Lexer::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'] === Lexer::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(Lexer::T_IS); $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_NULL); return new AST\NullComparisonExpression($expr, $not); } public function ExistsExpression() { $not = \false; if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); $not = \true; } $this->match(Lexer::T_EXISTS); $this->match(Lexer::T_OPEN_PARENTHESIS); $subselect = $this->Subselect(); $this->match(Lexer::T_CLOSE_PARENTHESIS); return new AST\ExistsExpression($subselect, $not); } public function ComparisonOperator() { assert($this->lexer->lookahead !== null); switch ($this->lexer->lookahead['value']) { case '=': $this->match(Lexer::T_EQUALS); return '='; case '<': $this->match(Lexer::T_LOWER_THAN); $operator = '<'; if ($this->lexer->isNextToken(Lexer::T_EQUALS)) { $this->match(Lexer::T_EQUALS); $operator .= '='; } elseif ($this->lexer->isNextToken(Lexer::T_GREATER_THAN)) { $this->match(Lexer::T_GREATER_THAN); $operator .= '>'; } return $operator; case '>': $this->match(Lexer::T_GREATER_THAN); $operator = '>'; if ($this->lexer->isNextToken(Lexer::T_EQUALS)) { $this->match(Lexer::T_EQUALS); $operator .= '='; } return $operator; case '!': $this->match(Lexer::T_NEGATE); $this->match(Lexer::T_EQUALS); return '<>'; default: $this->syntaxError('=, <, <=, <>, >, >=, !='); } } public function FunctionDeclaration() { assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $funcName = strtolower($token['value']); $customFunctionDeclaration = $this->CustomFunctionDeclaration(); // Check for custom functions functions first! switch (\true) { case $customFunctionDeclaration !== null: return $customFunctionDeclaration; case isset(self::$stringFunctions[$funcName]): return $this->FunctionsReturningStrings(); case isset(self::$numericFunctions[$funcName]): return $this->FunctionsReturningNumerics(); case isset(self::$datetimeFunctions[$funcName]): return $this->FunctionsReturningDatetime(); default: $this->syntaxError('known function', $token); } } private function CustomFunctionDeclaration() : ?Functions\FunctionNode { assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $funcName = strtolower($token['value']); // Check for custom functions afterwards $config = $this->em->getConfiguration(); switch (\true) { case $config->getCustomStringFunction($funcName) !== null: return $this->CustomFunctionsReturningStrings(); case $config->getCustomNumericFunction($funcName) !== null: return $this->CustomFunctionsReturningNumerics(); case $config->getCustomDatetimeFunction($funcName) !== null: return $this->CustomFunctionsReturningDatetime(); default: return null; } } public function FunctionsReturningNumerics() { assert($this->lexer->lookahead !== null); $funcNameLower = strtolower($this->lexer->lookahead['value']); $funcClass = self::$numericFunctions[$funcNameLower]; $function = new $funcClass($funcNameLower); $function->parse($this); return $function; } public function CustomFunctionsReturningNumerics() { assert($this->lexer->lookahead !== null); // getCustomNumericFunction is case-insensitive $functionName = strtolower($this->lexer->lookahead['value']); $functionClass = $this->em->getConfiguration()->getCustomNumericFunction($functionName); assert($functionClass !== null); $function = is_string($functionClass) ? new $functionClass($functionName) : $functionClass($functionName); $function->parse($this); return $function; } public function FunctionsReturningDatetime() { assert($this->lexer->lookahead !== null); $funcNameLower = strtolower($this->lexer->lookahead['value']); $funcClass = self::$datetimeFunctions[$funcNameLower]; $function = new $funcClass($funcNameLower); $function->parse($this); return $function; } public function CustomFunctionsReturningDatetime() { assert($this->lexer->lookahead !== null); // getCustomDatetimeFunction is case-insensitive $functionName = $this->lexer->lookahead['value']; $functionClass = $this->em->getConfiguration()->getCustomDatetimeFunction($functionName); assert($functionClass !== null); $function = is_string($functionClass) ? new $functionClass($functionName) : $functionClass($functionName); $function->parse($this); return $function; } public function FunctionsReturningStrings() { assert($this->lexer->lookahead !== null); $funcNameLower = strtolower($this->lexer->lookahead['value']); $funcClass = self::$stringFunctions[$funcNameLower]; $function = new $funcClass($funcNameLower); $function->parse($this); return $function; } public function CustomFunctionsReturningStrings() { assert($this->lexer->lookahead !== null); // getCustomStringFunction is case-insensitive $functionName = $this->lexer->lookahead['value']; $functionClass = $this->em->getConfiguration()->getCustomStringFunction($functionName); assert($functionClass !== null); $function = is_string($functionClass) ? new $functionClass($functionName) : $functionClass($functionName); $function->parse($this); return $function; } private function getMetadataForDqlAlias(string $dqlAlias) : ClassMetadata { if (!isset($this->queryComponents[$dqlAlias]['metadata'])) { throw new LogicException(sprintf('No metadata for DQL alias: %s', $dqlAlias)); } return $this->queryComponents[$dqlAlias]['metadata']; } }