. */ namespace Doctrine\ORM\Query; use Doctrine\ORM\Query\AST; use Doctrine\ORM\Exceptions\QueryException; use Doctrine\ORM\Query\Exec; /** * An LL(*) parser for the context-free grammar of Doctrine Query Language. * Parses a DQL query, reports any errors in it, and generates an AST. * * @author Guilherme Blanco * @author Janne Vanhala * @author Roman Borschel * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link http://www.doctrine-project.org * @since 2.0 * @version $Revision$ */ class Parser { /** * The minimum number of tokens read after last detected error before * another error can be reported. * * @var int */ //const MIN_ERROR_DISTANCE = 2; /** * Path expressions that were encountered during parsing of SelectExpressions * and still need to be validated. * * @var array */ private $_pendingPathExpressionsInSelect = array(); /** * A scanner object. * * @var Doctrine_ORM_Query_Scanner */ protected $_lexer; /** * The Parser Result object. * * @var Doctrine_ORM_Query_ParserResult */ protected $_parserResult; /** * Keyword symbol table * * @var Token */ protected $_keywordTable; /** * The EntityManager. * * @var EnityManager */ protected $_em; /** * Creates a new query parser object. * * @param string $dql DQL to be parsed. * @param Doctrine_Connection $connection The connection to use */ public function __construct(\Doctrine\ORM\Query $query) { $this->_em = $query->getEntityManager(); $this->_lexer = new Lexer($query->getDql()); $this->_keywordTable = new Token(); $defaultQueryComponent = ParserRule::DEFAULT_QUERYCOMPONENT; $this->_parserResult = new ParserResult( '', array( // queryComponent $defaultQueryComponent => array( 'metadata' => null, 'parent' => null, 'relation' => null, 'map' => null, 'scalar' => null, ), ), array( // tableAliasMap $defaultQueryComponent => $defaultQueryComponent, ) ); $this->_parserResult->setEntityManager($this->_em); $this->free(true); } /** * Attempts to match the given token with the current lookahead token. * * If they match, updates the lookahead token; otherwise raises a syntax * error. * * @param int|string token type or value * @return bool True, if tokens match; false otherwise. */ public function match($token) { if (is_string($token)) { $isMatch = ($this->_lexer->lookahead['value'] === $token); } else { $isMatch = ($this->_lexer->lookahead['type'] === $token); } if ( ! $isMatch) { // No definition for value checking. $this->syntaxError($this->_keywordTable->getLiteral($token)); } $this->_lexer->next(); return true; } public function isA($value, $token) { return $this->_lexer->isA($value, $token); } /** * Free this parser enabling it to be reused * * @param boolean $deep Whether to clean peek and reset errors * @param integer $position Position to reset * @return void */ public function free($deep = false, $position = 0) { // WARNING! Use this method with care. It resets the scanner! $this->_lexer->resetPosition($position); // Deep = true cleans peek and also any previously defined errors if ($deep) { $this->_lexer->resetPeek(); //$this->_errors = array(); } $this->_lexer->token = null; $this->_lexer->lookahead = null; //$this->_errorDistance = self::MIN_ERROR_DISTANCE; } /** * Parses a query string. */ public function parse() { // Parse & build AST $AST = $this->_QueryLanguage(); // Check for end of string if ($this->_lexer->lookahead !== null) { var_dump($this->_lexer->lookahead); $this->syntaxError('end of string'); } // Create SqlWalker who creates the SQL from the AST $sqlWalker = new SqlWalker($this->_em, $this->_parserResult); // Assign the executor in parser result $this->_parserResult->setSqlExecutor(Exec\AbstractExecutor::create($AST, $sqlWalker)); return $this->_parserResult; } /** * Returns the scanner object associated with this object. * * @return Doctrine_ORM_Query_Lexer */ public function getLexer() { return $this->_lexer; } /** * Returns the parser result associated with this object. * * @return Doctrine_ORM_Query_ParserResult */ public function getParserResult() { return $this->_parserResult; } /** * Generates a new syntax error. * * @param string $expected Optional expected string. * @param array $token Optional token. */ public function syntaxError($expected = '', $token = null) { if ($token === null) { $token = $this->_lexer->lookahead; } // Formatting message $message = 'line 0, col ' . (isset($token['position']) ? $token['position'] : '-1') . ': Error: '; if ($expected !== '') { $message .= "Expected '$expected', got "; } else { $message .= 'Unexpected '; } if ($this->_lexer->lookahead === null) { $message .= 'end of string.'; } else { $message .= "'{$this->_lexer->lookahead['value']}'"; } throw new QueryException($message); } /** * Generates a new semantical error. * * @param string $message Optional message. * @param array $token Optional token. */ public function semanticalError($message = '', $token = null) { if ($token === null) { $token = $this->_lexer->token; } //TODO: Include $token in $message throw new QueryException($message); } /** * Logs new error entry. * * @param string $message Message to log. * @param array $token Token that it was processing. */ /*protected function _logError($message = '', $token) { if ($this->_errorDistance >= self::MIN_ERROR_DISTANCE) { $message = 'line 0, col ' . $token['position'] . ': ' . $message; $this->_errors[] = $message; } $this->_errorDistance = 0; }*/ /** * Gets the EntityManager used by the parser. * * @return EntityManager */ public function getEntityManager() { return $this->_em; } /** * Retrieve the piece of DQL string given the token position * * @param array $token Token that it was processing. * @return string Piece of DQL string. */ /*public function getQueryPiece($token, $previousChars = 10, $nextChars = 10) { $start = max(0, $token['position'] - $previousChars); $end = max($token['position'] + $nextChars, strlen($this->_input)); return substr($this->_input, $start, $end); }*/ /** * Checks if the next-next (after lookahead) token start a function. * * @return boolean */ private function _isFunction() { $next = $this->_lexer->glimpse(); return ($next['value'] === '('); } /** * Checks whether the next 2 tokens start a subselect. * * @return boolean TRUE if the next 2 tokens start a subselect, FALSE otherwise. */ private function _isSubselect() { $la = $this->_lexer->lookahead; $next = $this->_lexer->glimpse(); return ($la['value'] === '(' && $next['type'] === Token::T_SELECT); } /* Parse methods */ /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement * * @return */ private function _QueryLanguage() { $this->_lexer->next(); switch ($this->_lexer->lookahead['type']) { case Token::T_SELECT: return $this->_SelectStatement(); break; case Token::T_UPDATE: return $this->_UpdateStatement(); break; case Token::T_DELETE: return $this->_DeleteStatement(); break; default: $this->syntaxError('SELECT, UPDATE or DELETE'); break; } } /** * SelectStatement ::= SelectClause FromClause [WhereClause] [GroupByClause] [HavingClause] [OrderByClause] * * @return */ private function _SelectStatement() { $selectClause = $this->_SelectClause(); $fromClause = $this->_FromClause(); $this->_processPendingPathExpressionsInSelect(); $whereClause = $this->_lexer->isNextToken(Token::T_WHERE) ? $this->_WhereClause() : null; $groupByClause = $this->_lexer->isNextToken(Token::T_GROUP) ? $this->_GroupByClause() : null; $havingClause = $this->_lexer->isNextToken(Token::T_HAVING) ? $this->_HavingClause() : null; $orderByClause = $this->_lexer->isNextToken(Token::T_ORDER) ? $this->_OrderByClause() : null; return new AST\SelectStatement( $selectClause, $fromClause, $whereClause, $groupByClause, $havingClause, $orderByClause ); } /** * Processes pending path expressions that were encountered while parsing * select expressions. These will be validated to make sure they are indeed * valid StateFieldPathExpressions and additional information * is attached to their AST nodes. */ private function _processPendingPathExpressionsInSelect() { $qComps = $this->_parserResult->getQueryComponents(); foreach ($this->_pendingPathExpressionsInSelect as $expr) { $parts = $expr->getParts(); $numParts = count($parts); $dqlAlias = $parts[0]; if (count($parts) == 2) { $expr->setIsSimpleStateFieldPathExpression(true); if ( ! $qComps[$dqlAlias]['metadata']->hasField($parts[1])) { $this->syntaxError(); } } else { $embeddedClassFieldSeen = false; $assocSeen = false; for ($i = 1; $i < $numParts - 1; ++$i) { if ($qComps[$dqlAlias]['metadata']->hasAssociation($parts[$i])) { if ($embeddedClassFieldSeen) { $this->semanticalError('Invalid navigation path.'); } // Indirect join $assoc = $qComps[$dqlAlias]['metadata']->getAssociationMapping($parts[$i]); if ( ! $assoc->isOneToOne()) { $this->semanticalError('Single-valued association expected.'); } $expr->setIsSingleValuedAssociationPart($parts[$i]); //TODO... $assocSeen = true; } else if ($qComps[$dqlAlias]['metadata']->hasEmbeddedClassField($parts[$i])) { //TODO... $expr->setIsEmbeddedClassPart($parts[$i]); $this->syntaxError(); } else { $this->syntaxError(); } } if ( ! $assocSeen) { $expr->setIsSimpleStateFieldPathExpression(true); } else { $expr->setIsSimpleStateFieldAssociationPathExpression(true); } // Last part MUST be a simple state field if ( ! $qComps[$dqlAlias]['metadata']->hasField($parts[$numParts-1])) { $this->syntaxError(); } } } } private function _UpdateStatement() { //TODO } private function _DeleteStatement() { //TODO } /** * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} */ private function _SelectClause() { $isDistinct = false; $this->match(Token::T_SELECT); // Inspecting if we are in a DISTINCT query if ($this->_lexer->isNextToken(Token::T_DISTINCT)) { $this->match(Token::T_DISTINCT); $isDistinct = true; } // Process SelectExpressions (1..N) $selectExpressions = array(); $selectExpressions[] = $this->_SelectExpression(); while ($this->_lexer->isNextToken(',')) { $this->match(','); $selectExpressions[] = $this->_SelectExpression(); } return new AST\SelectClause($selectExpressions, $isDistinct); } /** * FromClause ::= "FROM" IdentificationVariableDeclaration {"," IdentificationVariableDeclaration} */ private function _FromClause() { $this->match(Token::T_FROM); $identificationVariableDeclarations = array(); $identificationVariableDeclarations[] = $this->_IdentificationVariableDeclaration(); while ($this->_lexer->isNextToken(',')) { $this->match(','); $identificationVariableDeclarations[] = $this->_IdentificationVariableDeclaration(); } return new AST\FromClause($identificationVariableDeclarations); } /** * SelectExpression ::= * IdentificationVariable | StateFieldPathExpression | * (AggregateExpression | "(" Subselect ")") [["AS"] FieldAliasIdentificationVariable] */ private function _SelectExpression() { $expression = null; $fieldIdentificationVariable = null; $peek = $this->_lexer->glimpse(); // First we recognize for an IdentificationVariable (DQL class alias) if ($peek['value'] != '.' && $this->_lexer->lookahead['type'] === Token::T_IDENTIFIER) { $expression = $this->_IdentificationVariable(); } else if (($isFunction = $this->_isFunction()) !== false || $this->_isSubselect()) { $expression = $isFunction ? $this->_AggregateExpression() : $this->_Subselect(); if ($this->_lexer->isNextToken(Token::T_AS)) { $this->match(Token::T_AS); $fieldIdentificationVariable = $this->_FieldAliasIdentificationVariable(); } elseif ($this->_lexer->isNextToken(Token::T_IDENTIFIER)) { $fieldIdentificationVariable = $this->_FieldAliasIdentificationVariable(); } } else { $expression = $this->_PathExpressionInSelect(); } return new AST\SelectExpression($expression, $fieldIdentificationVariable); } /** * IdentificationVariable ::= identifier */ private function _IdentificationVariable() { $this->match(Token::T_IDENTIFIER); return $this->_lexer->token['value']; } /** * IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* */ private function _IdentificationVariableDeclaration() { $rangeVariableDeclaration = $this->_RangeVariableDeclaration(); $indexBy = $this->_lexer->isNextToken(Token::T_INDEX) ? $this->_IndexBy() : null; $joinVariableDeclarations = array(); while ( $this->_lexer->isNextToken(Token::T_LEFT) || $this->_lexer->isNextToken(Token::T_INNER) || $this->_lexer->isNextToken(Token::T_JOIN) ) { $joinVariableDeclarations[] = $this->_JoinVariableDeclaration(); } return new AST\IdentificationVariableDeclaration( $rangeVariableDeclaration, $indexBy, $joinVariableDeclarations ); } /** * RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable */ private function _RangeVariableDeclaration() { $abstractSchemaName = $this->_AbstractSchemaName(); if ($this->_lexer->isNextToken(Token::T_AS)) { $this->match(Token::T_AS); } $aliasIdentificationVariable = $this->_AliasIdentificationVariable(); $classMetadata = $this->_em->getClassMetadata($abstractSchemaName); // Building queryComponent $queryComponent = array( 'metadata' => $classMetadata, 'parent' => null, 'relation' => null, 'map' => null, 'scalar' => null, ); $this->_parserResult->setQueryComponent($aliasIdentificationVariable, $queryComponent); return new AST\RangeVariableDeclaration( $classMetadata, $aliasIdentificationVariable ); } /** * AbstractSchemaName ::= identifier */ private function _AbstractSchemaName() { $this->match(Token::T_IDENTIFIER); return $this->_lexer->token['value']; } /** * AliasIdentificationVariable = identifier */ private function _AliasIdentificationVariable() { $this->match(Token::T_IDENTIFIER); return $this->_lexer->token['value']; } /** * Special rule that acceps all kinds of path expressions. */ private function _PathExpression() { $this->match(Token::T_IDENTIFIER); $parts = array($this->_lexer->token['value']); while ($this->_lexer->isNextToken('.')) { $this->match('.'); $this->match(Token::T_IDENTIFIER); $parts[] = $this->_lexer->token['value']; } $pathExpression = new AST\PathExpression($parts); return $pathExpression; } /** * Special rule that acceps all kinds of path expressions. and defers their * semantical checking until the FROM part has been parsed completely (joins inclusive). * Mainly used for path expressions in the SelectExpressions. */ private function _PathExpressionInSelect() { $expr = $this->_PathExpression(); $this->_pendingPathExpressionsInSelect[] = $expr; return $expr; } /** * JoinVariableDeclaration ::= Join [IndexBy] */ private function _JoinVariableDeclaration() { $join = $this->_Join(); $indexBy = $this->_lexer->isNextToken(Token::T_INDEX) ? $this->_IndexBy() : null; return new AST\JoinVariableDeclaration($join, $indexBy); } /** * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] */ private function _Join() { // Check Join type $joinType = AST\Join::JOIN_TYPE_INNER; if ($this->_lexer->isNextToken(Token::T_LEFT)) { $this->match(Token::T_LEFT); // Possible LEFT OUTER join if ($this->_lexer->isNextToken(Token::T_OUTER)) { $this->match(Token::T_OUTER); $joinType = AST\Join::JOIN_TYPE_LEFTOUTER; } else { $joinType = AST\Join::JOIN_TYPE_LEFT; } } else if ($this->_lexer->isNextToken(Token::T_INNER)) { $this->match(Token::T_INNER); } $this->match(Token::T_JOIN); $joinPathExpression = $this->_JoinPathExpression(); if ($this->_lexer->isNextToken(Token::T_AS)) { $this->match(Token::T_AS); } $aliasIdentificationVariable = $this->_AliasIdentificationVariable(); // Verify that the association exists, if yes update the ParserResult // with the new component. $parentComp = $this->_parserResult->getQueryComponent($joinPathExpression->getIdentificationVariable()); $parentClass = $parentComp['metadata']; $assocField = $joinPathExpression->getAssociationField(); if ( ! $parentClass->hasAssociation($assocField)) { $this->semanticalError("Class " . $parentClass->getClassName() . " has no association named '$assocField'."); } $targetClassName = $parentClass->getAssociationMapping($assocField)->getTargetEntityName(); // Building queryComponent $joinQueryComponent = array( 'metadata' => $this->_em->getClassMetadata($targetClassName), 'parent' => $joinPathExpression->getIdentificationVariable(), 'relation' => $parentClass->getAssociationMapping($assocField), 'map' => null, 'scalar' => null, ); $this->_parserResult->setQueryComponent($aliasIdentificationVariable, $joinQueryComponent); // Create AST node $join = new AST\Join($joinType, $joinPathExpression, $aliasIdentificationVariable); // Check Join where type if ( $this->_lexer->isNextToken(Token::T_ON) || $this->_lexer->isNextToken(Token::T_WITH) ) { if ($this->_lexer->isNextToken(Token::T_ON)) { $this->match(Token::T_ON); $join->setWhereType(AST\Join::JOIN_WHERE_ON); } else { $this->match(Token::T_WITH); } $join->setConditionalExpression($this->_ConditionalExpression()); } return $join; } /*private function _JoinAssociationPathExpression() { if ($this->_isSingleValuedPathExpression()) { return $this->_JoinSingleValuedAssociationPathExpression(); } else { return $this->_JoinCollectionValuedPathExpression(); } }*/ /*private function _isSingleValuedPathExpression() { $parserResult = $this->_parserResult; // Trying to recoginize this grammar: // IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) $token = $this->lookahead; $this->_scanner->resetPeek(); if ($parserResult->hasQueryComponent($token['value'])) { $queryComponent = $parserResult->getQueryComponent($token['value']); $peek = $this->_scanner->peek(); if ($peek['value'] === '.') { $peek2 = $this->_scanner->peek(); if ($queryComponent['metadata']->hasAssociation($peek2['value']) && $queryComponent['metadata']->getAssociationMapping($peek2['value'])->isOneToOne()) { return true; } } } return false; }*/ /** * JoinPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) */ private function _JoinPathExpression() { $identificationVariable = $this->_IdentificationVariable(); $this->match('.'); $this->match(Token::T_IDENTIFIER); $assocField = $this->_lexer->token['value']; return new AST\JoinPathExpression( $identificationVariable, $assocField ); } /** * IndexBy ::= "INDEX" "BY" SimpleStateFieldPathExpression */ private function _IndexBy() { $this->match(Token::T_INDEX); $this->match(Token::T_BY); $pathExp = $this->_SimpleStateFieldPathExpression(); // Add the INDEX BY info to the query component $qComp = $this->_parserResult->getQueryComponent($pathExp->getIdentificationVariable()); $qComp['map'] = $pathExp->getSimpleStateField(); $this->_parserResult->setQueryComponent($pathExp->getIdentificationVariable(), $qComp); return $pathExp; } /** * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField */ private function _SimpleStateFieldPathExpression() { $identificationVariable = $this->_IdentificationVariable(); $this->match('.'); $this->match(Token::T_IDENTIFIER); $simpleStateField = $this->_lexer->token['value']; return new AST\SimpleStateFieldPathExpression($identificationVariable, $simpleStateField); } /** * StateFieldPathExpression ::= SimpleStateFieldPathExpression | SimpleStateFieldAssociationPathExpression */ private function _StateFieldPathExpression() { $parts = array(); $stateFieldSeen = false; $assocSeen = false; $identificationVariable = $this->_IdentificationVariable(); if ( ! $this->_parserResult->hasQueryComponent($identificationVariable)) { $this->syntaxError("Identification variable."); } $qComp = $this->_parserResult->getQueryComponent($identificationVariable); $parts[] = $identificationVariable; $class = $qComp['metadata']; if ( ! $this->_lexer->isNextToken('.')) { $this->syntaxError(); } while ($this->_lexer->isNextToken('.')) { if ($stateFieldSeen) $this->syntaxError(); $this->match('.'); $part = $this->_IdentificationVariable(); if ($class->hasField($part)) { $stateFieldSeen = true; } else if ($class->hasAssociation($part)) { $assoc = $class->getAssociationMapping($part); $class = $this->_em->getClassMetadata($assoc->getTargetEntityName()); $assocSeen = true; } else { $this->syntaxError(); } $parts[] = $part; } $pathExpr = new AST\PathExpression($parts); if ($assocSeen) { $pathExpr->setIsSimpleStateFieldAssociationPathExpression(true); } else { $pathExpr->setIsSimpleStateFieldPathExpression(true); } return $pathExpr; } /** * AggregateExpression ::= * ("AVG" | "MAX" | "MIN" | "SUM") "(" ["DISTINCT"] StateFieldPathExpression ")" | * "COUNT" "(" ["DISTINCT"] (IdentificationVariable | SingleValuedAssociationPathExpression | StateFieldPathExpression) ")" */ private function _AggregateExpression() { $isDistinct = false; $functionName = ''; if ($this->_lexer->isNextToken(Token::T_COUNT)) { $this->match(Token::T_COUNT); $functionName = $this->_lexer->token['value']; $this->match('('); if ($this->_lexer->isNextToken(Token::T_DISTINCT)) { $this->match(Token::T_DISTINCT); $isDistinct = true; } // For now we only support a PathExpression here... $pathExp = $this->_PathExpression(); $this->match(')'); } else if ($this->_lexer->isNextToken(Token::T_AVG)) { $this->match(Token::T_AVG); $functionName = $this->_lexer->token['value']; $this->match('('); //... } else { $this->syntaxError('One of: MAX, MIN, AVG, SUM, COUNT'); } return new AST\AggregateExpression($functionName, $pathExp, $isDistinct); } /** * GroupByClause ::= "GROUP" "BY" GroupByItem {"," GroupByItem}* * GroupByItem ::= SingleValuedPathExpression */ private function _GroupByClause() { $this->match(Token::T_GROUP); $this->match(Token::T_BY); $groupByItems = array(); $groupByItems[] = $this->_PathExpression(); while ($this->_lexer->isNextToken(',')) { $this->match(','); $groupByItems[] = $this->_PathExpression(); } return new AST\GroupByClause($groupByItems); } /** * WhereClause ::= "WHERE" ConditionalExpression */ private function _WhereClause() { $this->match(Token::T_WHERE); return new AST\WhereClause($this->_ConditionalExpression()); } /** * ConditionalExpression ::= ConditionalTerm {"OR" ConditionalTerm}* */ private function _ConditionalExpression() { $conditionalTerms = array(); $conditionalTerms[] = $this->_ConditionalTerm(); while ($this->_lexer->isNextToken(Token::T_OR)) { $this->match(Token::T_OR); $conditionalTerms[] = $this->_ConditionalTerm(); } return new AST\ConditionalExpression($conditionalTerms); } /** * ConditionalTerm ::= ConditionalFactor {"AND" ConditionalFactor}* */ private function _ConditionalTerm() { $conditionalFactors = array(); $conditionalFactors[] = $this->_ConditionalFactor(); while ($this->_lexer->isNextToken(Token::T_AND)) { $this->match(Token::T_AND); $conditionalFactors[] = $this->_ConditionalFactor(); } return new AST\ConditionalTerm($conditionalFactors); } /** * ConditionalFactor ::= ["NOT"] ConditionalPrimary */ private function _ConditionalFactor() { $not = false; if ($this->_lexer->isNextToken(Token::T_NOT)) { $this->match(Token::T_NOT); $not = true; } return new AST\ConditionalFactor($this->_ConditionalPrimary(), $not); } /** * ConditionalPrimary ::= SimpleConditionalExpression | "(" ConditionalExpression ")" */ private function _ConditionalPrimary() { $condPrimary = new AST\ConditionalPrimary; if ($this->_lexer->isNextToken('(')) { $numUnmatched = 1; $peek = $this->_lexer->peek(); while ($numUnmatched > 0) { if ($peek['value'] == ')') { --$numUnmatched; } else if ($peek['value'] == '(') { ++$numUnmatched; } $peek = $this->_lexer->peek(); } $this->_lexer->resetPeek(); //TODO: This is not complete, what about LIKE/BETWEEN/...etc? $comparisonOps = array("=", "<", "<=", "<>", ">", ">=", "!="); if (in_array($peek['value'], $comparisonOps)) { $condPrimary->setSimpleConditionalExpression($this->_SimpleConditionalExpression()); } else { $this->match('('); $conditionalExpression = $this->_ConditionalExpression(); $this->match(')'); $condPrimary->setConditionalExpression($conditionalExpression); } } else { $condPrimary->setSimpleConditionalExpression($this->_SimpleConditionalExpression()); } return $condPrimary; } /** * SimpleConditionalExpression ::= * ComparisonExpression | BetweenExpression | LikeExpression | * InExpression | NullComparisonExpression | ExistsExpression | * EmptyCollectionComparisonExpression | CollectionMemberExpression */ private function _SimpleConditionalExpression() { if ($this->_lexer->isNextToken(Token::T_NOT)) { $token = $this->_lexer->glimpse(); } else { $token = $this->_lexer->lookahead; } if ($token['type'] === Token::T_EXISTS) { return $this->_ExistsExpression(); } $stateFieldPathExpr = false; if ($token['type'] === Token::T_IDENTIFIER) { // Peek beyond the PathExpression $stateFieldPathExpr = true; $peek = $this->_lexer->peek(); while ($peek['value'] === '.') { $this->_lexer->peek(); $peek = $this->_lexer->peek(); } $this->_lexer->resetPeek(); $token = $peek; } if ($stateFieldPathExpr) { switch ($token['type']) { case Token::T_BETWEEN: return $this->_BetweenExpression(); case Token::T_LIKE: return $this->_LikeExpression(); case Token::T_IN: return $this->_InExpression(); case Token::T_IS: return $this->_NullComparisonExpression(); case Token::T_NONE: return $this->_ComparisonExpression(); default: $this->syntaxError(); } } else if ($token['value'] == '(') { return $this->_ComparisonExpression(); } else { switch ($token['type']) { case Token::T_INTEGER: // IF it turns out its a ComparisonExpression, then it MUST be ArithmeticExpression break; case Token::T_STRING: // IF it turns out its a ComparisonExpression, then it MUST be StringExpression break; default: $this->syntaxError(); } } } /** * SIMPLIFIED FROM BNF FOR NOW * ComparisonExpression ::= ArithmeticExpression ComparisonOperator ( QuantifiedExpression | ArithmeticExpression ) */ private function _ComparisonExpression() { $leftExpr = $this->_ArithmeticExpression(); $operator = $this->_ComparisonOperator(); if ($this->_lexer->lookahead['type'] === Token::T_ALL || $this->_lexer->lookahead['type'] === Token::T_ANY || $this->_lexer->lookahead['type'] === Token::T_SOME) { $rightExpr = $this->_QuantifiedExpression(); } else { $rightExpr = $this->_ArithmeticExpression(); } return new AST\ComparisonExpression($leftExpr, $operator, $rightExpr); } /** * ArithmeticExpression ::= SimpleArithmeticExpression | "(" Subselect ")" */ private function _ArithmeticExpression() { $expr = new AST\ArithmeticExpression; if ($this->_lexer->lookahead['value'] === '(') { $peek = $this->_lexer->glimpse(); if ($peek['type'] === Token::T_SELECT) { $expr->setSubselect($this->_Subselect()); return $expr; } } $expr->setSimpleArithmeticExpression($this->_SimpleArithmeticExpression()); return $expr; } /** * SimpleArithmeticExpression ::= ArithmeticTerm {("+" | "-") ArithmeticTerm}* */ private function _SimpleArithmeticExpression() { $terms = array(); $terms[] = $this->_ArithmeticTerm(); while ($this->_lexer->lookahead['value'] == '+' || $this->_lexer->lookahead['value'] == '-') { if ($this->_lexer->lookahead['value'] == '+') { $this->match('+'); } else { $this->match('-'); } $terms[] = $this->_lexer->token['value']; $terms[] = $this->_ArithmeticTerm(); } return new AST\SimpleArithmeticExpression($terms); } /** * ArithmeticTerm ::= ArithmeticFactor {("*" | "/") ArithmeticFactor}* */ private function _ArithmeticTerm() { $factors = array(); $factors[] = $this->_ArithmeticFactor(); while ($this->_lexer->lookahead['value'] == '*' || $this->_lexer->lookahead['value'] == '/') { if ($this->_lexer->lookahead['value'] == '*') { $this->match('*'); } else { $this->match('/'); } $factors[] = $this->_lexer->token['value']; $factors[] = $this->_ArithmeticFactor(); } return new AST\ArithmeticTerm($factors); } /** * ArithmeticFactor ::= [("+" | "-")] ArithmeticPrimary */ private function _ArithmeticFactor() { $pSign = $nSign = false; if ($this->_lexer->lookahead['value'] == '+') { $this->match('+'); $pSign = true; } else if ($this->_lexer->lookahead['value'] == '-') { $this->match('-'); $nSign = true; } return new AST\ArithmeticFactor($this->_ArithmeticPrimary(), $pSign, $nSign); } /** * ArithmeticPrimary ::= StateFieldPathExpression | Literal | "(" SimpleArithmeticExpression ")" | Function | AggregateExpression */ private function _ArithmeticPrimary() { if ($this->_lexer->lookahead['value'] === '(') { $this->match('('); $expr = $this->_SimpleArithmeticExpression(); $this->match(')'); return $expr; } switch ($this->_lexer->lookahead['type']) { case Token::T_IDENTIFIER: $peek = $this->_lexer->glimpse(); if ($peek['value'] == '(') { if ($this->_isAggregateFunction($peek['type'])) { return $this->_AggregateExpression(); } return $this->_FunctionsReturningStrings(); } return $this->_StateFieldPathExpression(); case Token::T_INPUT_PARAMETER: $this->match($this->_lexer->lookahead['value']); return new AST\InputParameter($this->_lexer->token['value']); case Token::T_STRING: case Token::T_INTEGER: case Token::T_FLOAT: $this->match($this->_lexer->lookahead['value']); return $this->_lexer->token['value']; default: $this->syntaxError(); } throw new Doctrine_Exception("Not yet implemented."); //TODO... } /** * PortableFunctionsReturningStrings ::= * "CONCAT" "(" StringPrimary "," StringPrimary ")" | * "SUBSTRING" "(" StringPrimary "," SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | * "TRIM" "(" [["LEADING" | "TRAILING" | "BOTH"] [char] "FROM"] StringPrimary ")" | * "LOWER" "(" StringPrimary ")" | * "UPPER" "(" StringPrimary ")" */ private function _FunctionsReturningStrings() { switch (strtoupper($this->_lexer->lookahead['value'])) { case 'CONCAT': break; case 'SUBSTRING': break; case 'TRIM': $this->match($this->_lexer->lookahead['value']); $this->match('('); //TODO: This is not complete! See BNF $this->_StringPrimary(); break; case 'LOWER': break; case 'UPPER': default: $this->syntaxError('CONCAT, SUBSTRING, TRIM or UPPER'); } } private function _isAggregateFunction($tokenType) { switch ($tokenType) { case Token::T_AVG: case Token::T_MIN: case Token::T_MAX: case Token::T_SUM: case Token::T_COUNT: return true; default: return false; } } /** * ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" */ private function _ComparisonOperator() { switch ($this->_lexer->lookahead['value']) { case '=': $this->match('='); return '='; case '<': $this->match('<'); $operator = '<'; if ($this->_lexer->isNextToken('=')) { $this->match('='); $operator .= '='; } else if ($this->_lexer->isNextToken('>')) { $this->match('>'); $operator .= '>'; } return $operator; case '>': $this->match('>'); $operator = '>'; if ($this->_lexer->isNextToken('=')) { $this->match('='); $operator .= '='; } return $operator; case '!': $this->match('!'); $this->match('='); return '<>'; default: $this->syntaxError('=, <, <=, <>, >, >=, !='); break; } } /** * LikeExpression ::= StringExpression ["NOT"] "LIKE" string ["ESCAPE" char] */ private function _LikeExpression() { $stringExpr = $this->_StringExpression(); $isNot = false; if ($this->_lexer->lookahead['type'] === Token::T_NOT) { $this->match(Token::T_NOT); $isNot = true; } $this->match(Token::T_LIKE); $this->match(Token::T_STRING); $stringPattern = $this->_lexer->token['value']; $escapeChar = null; if ($this->_lexer->lookahead['type'] === Token::T_ESCAPE) { $this->match(Token::T_ESCAPE); var_dump($this->_lexer->lookahead); //$this->match(Token::T_) //$escapeChar = } return new AST\LikeExpression($stringExpr, $stringPattern, $isNot, $escapeChar); } /** * StringExpression ::= StringPrimary | "(" Subselect ")" */ private function _StringExpression() { if ($this->_lexer->lookahead['value'] === '(') { $peek = $this->_lexer->glimpse(); if ($peek['type'] === Token::T_SELECT) { return $this->_Subselect(); } } return $this->_StringPrimary(); } /** * StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression */ private function _StringPrimary() { if ($this->_lexer->lookahead['type'] === Token::T_IDENTIFIER) { $peek = $this->_lexer->glimpse(); if ($peek['value'] == '.') { return $this->_StateFieldPathExpression(); } else if ($peek['value'] == '(') { //TODO... FunctionsReturningStrings or AggregateExpression } else { $this->syntaxError("'.' or '('"); } } else if ($this->_lexer->lookahead['type'] === Token::T_STRING) { //TODO... } else if ($this->_lexer->lookahead['type'] === Token::T_INPUT_PARAMETER) { //TODO... } else { $this->syntaxError('StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression'); } } }