diff --git a/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php similarity index 59% rename from lib/Doctrine/ORM/Query/AST/JoinPathExpression.php rename to lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php index d68c88c68..7aab445a3 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinPathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/JoinAssociationPathExpression.php @@ -3,14 +3,12 @@ namespace Doctrine\ORM\Query\AST; /** - * JoinAssociationPathExpression ::= JoinCollectionValuedPathExpression | JoinSingleValuedAssociationPathExpression - * JoinCollectionValuedPathExpression ::= IdentificationVariable "." CollectionValuedAssociationField - * JoinSingleValuedAssociationPathExpression ::= IdentificationVariable "." SingleValuedAssociationField + * JoinAssociationPathExpression ::= IdentificationVariable "." (SingleValuedAssociationField | CollectionValuedAssociationField) * - * @author robo - * @todo Rename: JoinAssociationPathExpression + * @author Guilherme Blanco + * @author Roman Borschel */ -class JoinPathExpression extends Node +class JoinAssociationPathExpression extends Node { private $_identificationVariable; private $_assocField; diff --git a/lib/Doctrine/ORM/Query/AST/PathExpression.php b/lib/Doctrine/ORM/Query/AST/PathExpression.php index a8833f6f6..d7404fac1 100644 --- a/lib/Doctrine/ORM/Query/AST/PathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/PathExpression.php @@ -26,18 +26,18 @@ namespace Doctrine\ORM\Query\AST; */ class PathExpression extends Node { - const TYPE_SINGLE_VALUED_PATH_EXPRESSION = 1; const TYPE_COLLECTION_VALUED_ASSOCIATION = 2; const TYPE_SINGLE_VALUED_ASSOCIATION = 4; const TYPE_STATE_FIELD = 8; private $_type; + private $_expectedType; private $_identificationVariable; private $_parts; - public function __construct($type, $identificationVariable, array $parts) + public function __construct($expectedType, $identificationVariable, array $parts) { - $this->_type = $type; + $this->_expectedType = $expectedType; $this->_identificationVariable = $identificationVariable; $this->_parts = $parts; } @@ -52,6 +52,19 @@ class PathExpression extends Node return $this->_parts; } + public function setExpectedType($type) + { + $this->_expectedType; + } + + public function getExpectedType() + { + return $this->_expectedType; + } + + /** + * INTERNAL + */ public function setType($type) { $this->_type = $type; diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 8bca73a75..4b6c61db2 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -316,7 +316,7 @@ class Parser // Building informative message $message = 'line 0, col ' . (isset($token['position']) ? $token['position'] : '-1') - . " near '" . substr($dql, $token['position'], $length) . "'): Error: " . $message; + . " near '" . substr($dql, $token['position'], $length) . "': Error: " . $message; throw \Doctrine\ORM\Query\QueryException::semanticalError($message); } @@ -440,27 +440,7 @@ class Parser $exprStack = array_pop($this->_deferredPathExpressionStacks); foreach ($exprStack as $expr) { - switch ($expr->getType()) { - case AST\PathExpression::TYPE_SINGLE_VALUED_PATH_EXPRESSION: - $this->_validateSingleValuedPathExpression($expr); - break; - - case AST\PathExpression::TYPE_STATE_FIELD: - $this->_validateStateFieldPathExpression($expr); - break; - - case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: - $this->_validateSingleValuedAssociationPathExpression($expr); - break; - - case AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION: - $this->_validateCollectionValuedAssociationPathExpression($expr); - break; - - default: - $this->semanticalError('Encountered invalid PathExpression.'); - break; - } + $this->_validatePathExpression($expr); } } @@ -474,11 +454,15 @@ class Parser * CollectionValuedPathExpression ::= IdentificationVariable "." {SingleValuedAssociationField "."}* CollectionValuedAssociationField * * @param PathExpression $pathExpression - * @return boolean + * @return integer */ private function _validatePathExpression(AST\PathExpression $pathExpression) { - $class = $this->_queryComponents[$pathExpression->getIdentificationVariable()]['metadata']; + $identificationVariable = $pathExpression->getIdentificationVariable(); + + $this->_validateIdentificationVariable($identificationVariable); + + $class = $this->_queryComponents[$identificationVariable]['metadata']; $stateField = $collectionField = null; foreach ($pathExpression->getParts() as $field) { @@ -506,6 +490,7 @@ class Parser } } + // Recognize correct expression type $expressionType = null; if ($stateField !== null) { @@ -514,78 +499,65 @@ class Parser $expressionType = AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION; } else { $expressionType = AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION; - } + } + + // Validate if PathExpression is one of the expected types + $expectedType = $pathExpression->getExpectedType(); + + if ( ! ($expectedType & $expressionType)) { + // We need to recognize which was expected type(s) + $expectedStringTypes = array(); + + // Validate state field type (field/column) + 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.'; + + if (count($expectedStringTypes) == 1) { + $semanticalError .= ' Must be a ' . $expectedStringTypes[0] . '.'; + } else { + $semanticalError .= ' ' . implode(' or ', $expectedStringTypes) . ' expected.'; + } + + $this->semanticalError($semanticalError); + } // We need to force the type in PathExpression $pathExpression->setType($expressionType); return $expressionType; } - - /** - * Validates that the given PathExpression is a semantically correct - * SingleValuedPathExpression as specified in the grammar. - * - * @param PathExpression $pathExpression - */ - private function _validateSingleValuedPathExpression(AST\PathExpression $pathExpression) - { - $pathExprType = $this->_validatePathExpression($pathExpression); - - if ( - $pathExprType !== AST\PathExpression::TYPE_STATE_FIELD && - $pathExprType !== AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION - ) { - $this->semanticalError('SingleValuedAssociationField or StateField expected.'); - } - } /** - * Validates that the given PathExpression is a semantically correct - * StateFieldPathExpression as specified in the grammar. + * Validates that the given IdentificationVariable is a semantically correct. + * It must exist in query components list. * - * @param PathExpression $pathExpression + * @param string $identificationVariable */ - private function _validateStateFieldPathExpression(AST\PathExpression $pathExpression) + private function _validateIdentificationVariable($identificationVariable) { - $pathExprType = $this->_validatePathExpression($pathExpression); - - if ($pathExprType !== AST\PathExpression::TYPE_STATE_FIELD) { - $this->semanticalError('Invalid StateFieldPathExpression. Must end in a state field.'); + if ( ! isset($this->_queryComponents[$identificationVariable])) { + $this->semanticalError( + 'Invalid IdentificationVariable. Could not find \'' . + $identificationVariable . '\' in query components.' + ); } } + - /** - * Validates that the given PathExpression is a semantically correct - * CollectionValuedPathExpression as specified in the grammar. - * - * @param PathExpression $pathExpression - */ - private function _validateCollectionValuedPathExpression(AST\PathExpression $pathExpression) - { - $pathExprType = $this->_validatePathExpression($pathExpression); - - if ($pathExprType !== AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION) { - $this->semanticalError('Invalid CollectionValuedAssociationField. Must end in a collection-valued field.'); - } - } - - /** - * Validates that the given PathExpression is a semantically correct - * SingleValuedAssociationPathExpression as specified in the grammar. - * - * @param PathExpression $pathExpression - */ - private function _validateSingleValuedAssociationPathExpression(AST\PathExpression $pathExpression) - { - $pathExprType = $this->_validatePathExpression($pathExpression); - - if ($pathExprType !== AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION) { - $this->semanticalError('Invalid SingleValuedAssociationField. Must end in a single valued association field.'); - } - } - - /** * QueryLanguage ::= SelectStatement | UpdateStatement | DeleteStatement */ @@ -711,30 +683,40 @@ class Parser /** - * JoinPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) + * JoinAssociationPathExpression ::= IdentificationVariable "." (CollectionValuedAssociationField | SingleValuedAssociationField) */ - public function JoinPathExpression() + public function JoinAssociationPathExpression() { $identificationVariable = $this->IdentificationVariable(); $this->match('.'); $this->match(Lexer::T_IDENTIFIER); - - return new AST\JoinPathExpression( - $identificationVariable, $this->_lexer->token['value'] - ); + $field = $this->_lexer->token['value']; + + // Validating IdentificationVariable (it was already defined previously) + $this->_validateIdentificationVariable($identificationVariable); + + // Validating association field (*-to-one or *-to-many) + $class = $this->_queryComponents[$identificationVariable]['metadata']; + + if ( ! isset($class->associationMappings[$field])) { + $this->semanticalError('Class ' . $class->name . ' has no field named ' . $field); + } + + return new AST\JoinAssociationPathExpression($identificationVariable, $field); } /** - * Parses an arbitrary path expression without any semantic validation. + * Parses an arbitrary path expression. Applies or defer semantical validation + * based on expected types. * * PathExpression ::= IdentificationVariable "." {identifier "."}* identifier * + * @param integer $expectedType * @return PathExpression */ - public function PathExpression($type) + public function PathExpression($expectedType) { $identificationVariable = $this->IdentificationVariable(); - $parts = array(); do { @@ -744,17 +726,33 @@ class Parser $parts[] = $this->_lexer->token['value']; } while ($this->_lexer->isNextToken('.')); - return new AST\PathExpression($type, $identificationVariable, $parts); + // Creating AST node + $pathExpr = new AST\PathExpression($expectedType, $identificationVariable, $parts); + + // Defer PathExpression validation if requested to be defered + if ( ! empty($this->_deferredPathExpressionStacks)) { + $exprStack = array_pop($this->_deferredPathExpressionStacks); + $exprStack[] = $pathExpr; + array_push($this->_deferredPathExpressionStacks, $exprStack); + + return $pathExpr; + } + + // Apply PathExpression validation normally (not in defer mode) + $this->_validatePathExpression($pathExpr); + + return $pathExpr; } /** * AssociationPathExpression ::= CollectionValuedPathExpression | SingleValuedAssociationPathExpression - * - * @todo Implement this grammar rule which is used in SubselectFromClause */ public function AssociationPathExpression() { - + return $this->PathExpression( + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION | + AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION + ); } /** @@ -762,19 +760,10 @@ class Parser */ public function SingleValuedPathExpression() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_PATH_EXPRESSION); - - if ( ! empty($this->_deferredPathExpressionStacks)) { - $exprStack = array_pop($this->_deferredPathExpressionStacks); - $exprStack[] = $pathExpr; - array_push($this->_deferredPathExpressionStacks, $exprStack); - - return $pathExpr; - } - - $this->_validateSingleValuedPathExpression($pathExpr); - - return $pathExpr; + return $this->PathExpression( + AST\PathExpression::TYPE_STATE_FIELD | + AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION + ); } /** @@ -782,19 +771,7 @@ class Parser */ public function StateFieldPathExpression() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); - - if ( ! empty($this->_deferredPathExpressionStacks)) { - $exprStack = array_pop($this->_deferredPathExpressionStacks); - $exprStack[] = $pathExpr; - array_push($this->_deferredPathExpressionStacks, $exprStack); - - return $pathExpr; - } - - $this->_validateStateFieldPathExpression($pathExpr); - - return $pathExpr; + return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); } /** @@ -802,19 +779,7 @@ class Parser */ public function SingleValuedAssociationPathExpression() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); - - if ( ! empty($this->_deferredPathExpressionStacks)) { - $exprStack = array_pop($this->_deferredPathExpressionStacks); - $exprStack[] = $pathExpr; - array_push($this->_deferredPathExpressionStacks, $exprStack); - - return $pathExpr; - } - - $this->_validateSingleValuedAssociationPathExpression($pathExpr); - - return $pathExpr; + return $this->PathExpression(AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION); } /** @@ -822,39 +787,17 @@ class Parser */ public function CollectionValuedPathExpression() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); - - if ( ! empty($this->_deferredPathExpressionStacks)) { - $exprStack = array_pop($this->_deferredPathExpressionStacks); - $exprStack[] = $pathExpr; - array_push($this->_deferredPathExpressionStacks, $exprStack); - - return $pathExpr; - } - - $this->_validateCollectionValuedPathExpression($pathExpr); - - return $pathExpr; + return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); } /** * SimpleStateFieldPathExpression ::= IdentificationVariable "." StateField + * + * @todo We should only allow 1 single part here (state field). It is used by INDEX BY clause. */ public function SimpleStateFieldPathExpression() { - $pathExpr = $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); - - if ( ! empty($this->_deferredPathExpressionStacks)) { - $exprStack = array_pop($this->_deferredPathExpressionStacks); - $exprStack[] = $pathExpr; - array_push($this->_deferredPathExpressionStacks, $exprStack); - - return $pathExpr; - } - - $this->_validateStateFieldPathExpression($pathExpr); - - return $pathExpr; + return $this->PathExpression(AST\PathExpression::TYPE_STATE_FIELD); } @@ -1288,7 +1231,7 @@ class Parser } /** - * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinPathExpression + * Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression * ["AS"] AliasIdentificationVariable [("ON" | "WITH") ConditionalExpression] */ public function Join() @@ -1311,7 +1254,7 @@ class Parser } $this->match(Lexer::T_JOIN); - $joinPathExpression = $this->JoinPathExpression(); + $joinPathExpression = $this->JoinAssociationPathExpression(); if ($this->_lexer->isNextToken(Lexer::T_AS)) { $this->match(Lexer::T_AS);