From 6a8ee87268bdb1bd40eee474fca9a853711c6dfa Mon Sep 17 00:00:00 2001 From: Guilherme Blanco Date: Sun, 20 Apr 2014 20:27:57 +0000 Subject: [PATCH] Made DQL and EBNF in sync. --- .../reference/dql-doctrine-query-language.rst | 26 +++--- .../ORM/Query/AST/JoinVariableDeclaration.php | 61 ++++++++++++++ ...electIdentificationVariableDeclaration.php | 52 ++++++++++++ lib/Doctrine/ORM/Query/Parser.php | 78 +++++++++++++---- lib/Doctrine/ORM/Query/SqlWalker.php | 83 +++++++++++-------- .../ORM/Query/CustomTreeWalkersJoinTest.php | 63 +++++++------- .../Tests/ORM/Query/CustomTreeWalkersTest.php | 65 ++++++++------- 7 files changed, 304 insertions(+), 124 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php create mode 100644 lib/Doctrine/ORM/Query/AST/SubselectIdentificationVariableDeclaration.php diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 69d115053..aacd4d89b 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -1440,6 +1440,12 @@ Identifiers /* identifier that must be a class name (the "User" of "FROM User u") */ AbstractSchemaName ::= identifier + /* Alias ResultVariable declaration (the "total" of "COUNT(*) AS total") */ + AliasResultVariable = identifier + + /* ResultVariable identifier usage of mapped field aliases (the "total" of "COUNT(*) AS total") */ + ResultVariable = identifier + /* identifier that must be a field (the "name" of "u.name") */ /* This is responsible to know if the field exists in Object, no matter if it's a relation or a simple field */ FieldIdentificationVariable ::= identifier @@ -1450,19 +1456,13 @@ Identifiers /* identifier that must be a single-valued association field (to-one) (the "Group" of "u.Group") */ SingleValuedAssociationField ::= FieldIdentificationVariable - /* identifier that must be an embedded class state field (for the future) */ + /* identifier that must be an embedded class state field */ EmbeddedClassStateField ::= FieldIdentificationVariable /* identifier that must be a simple state field (name, email, ...) (the "name" of "u.name") */ /* The difference between this and FieldIdentificationVariable is only semantical, because it points to a single field (not mapping to a relation) */ SimpleStateField ::= FieldIdentificationVariable - /* Alias ResultVariable declaration (the "total" of "COUNT(*) AS total") */ - AliasResultVariable = identifier - - /* ResultVariable identifier usage of mapped field aliases (the "total" of "COUNT(*) AS total") */ - ResultVariable = identifier - Path Expressions ~~~~~~~~~~~~~~~~ @@ -1521,11 +1521,11 @@ From, Join and Index by .. code-block:: php - IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {JoinVariableDeclaration}* - SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) - JoinVariableDeclaration ::= Join [IndexBy] + IdentificationVariableDeclaration ::= RangeVariableDeclaration [IndexBy] {Join}* + SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable - Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" JoinAssociationPathExpression ["AS"] AliasIdentificationVariable ["WITH" ConditionalExpression] + JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy] + Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration | RangeVariableDeclaration) ["WITH" ConditionalExpression] IndexBy ::= "INDEX" "BY" StateFieldPathExpression Select Expressions @@ -1533,10 +1533,12 @@ Select Expressions .. code-block:: php - SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression) [["AS"] ["HIDDEN"] AliasResultVariable] + SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable] SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable] PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" + NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" + NewObjectArg ::= ScalarExpression | "(" Subselect ")" Conditional Expressions ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php b/lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php new file mode 100644 index 000000000..89aa83ad8 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/JoinVariableDeclaration.php @@ -0,0 +1,61 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * JoinVariableDeclaration ::= Join [IndexBy] + * + * @license http://www.opensource.org/licenses/mit-license.php MIT + * @link www.doctrine-project.org + * @since 2.5 + * @author Guilherme Blanco + */ +class JoinVariableDeclaration extends Node +{ + /** + * @var Join + */ + public $join; + + /** + * @var IndexBy|null + */ + public $indexBy; + + /** + * Constructor. + * + * @param Join $join + * @param IndexBy|null $indexBy + */ + public function __construct($join, $indexBy) + { + $this->join = $join; + $this->indexBy = $indexBy; + } + + /** + * {@inheritdoc} + */ + public function dispatch($walker) + { + return $walker->walkJoinVariableDeclaration($this); + } +} diff --git a/lib/Doctrine/ORM/Query/AST/SubselectIdentificationVariableDeclaration.php b/lib/Doctrine/ORM/Query/AST/SubselectIdentificationVariableDeclaration.php new file mode 100644 index 000000000..8fbd9e210 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/SubselectIdentificationVariableDeclaration.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * SubselectIdentificationVariableDeclaration ::= AssociationPathExpression ["AS"] AliasIdentificationVariable + * + * @link www.doctrine-project.org + * @since 2.0 + * @author Guilherme Blanco + */ +class SubselectIdentificationVariableDeclaration +{ + /** + * @var PathExpression + */ + public $associationPathExpression; + + /** + * @var string + */ + public $aliasIdentificationVariable; + + /** + * Constructor. + * + * @param PathExpression $associationPathExpression + * @param string $aliasIdentificationVariable + */ + public function __construct($associationPathExpression, $aliasIdentificationVariable) + { + $this->associationPathExpression = $associationPathExpression; + $this->aliasIdentificationVariable = $aliasIdentificationVariable; + } +} diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index d010ad45d..6a1e32acd 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -1049,7 +1049,7 @@ class Parser * Parses an arbitrary path expression and defers semantical validation * based on expected types. * - * PathExpression ::= IdentificationVariable "." identifier [ ("." identifier)* ] + * PathExpression ::= IdentificationVariable {"." identifier}* * * @param integer $expectedTypes * @@ -1553,13 +1553,14 @@ class Parser */ public function IdentificationVariableDeclaration() { + $joins = array(); $rangeVariableDeclaration = $this->RangeVariableDeclaration(); + $indexBy = $this->lexer->isNextToken(Lexer::T_INDEX) + ? $this->IndexBy() + : null; $rangeVariableDeclaration->isRoot = true; - $indexBy = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; - $joins = array(); - while ( $this->lexer->isNextToken(Lexer::T_LEFT) || $this->lexer->isNextToken(Lexer::T_INNER) || @@ -1574,24 +1575,59 @@ class Parser } /** + * + * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration + * + * {@internal WARNING: Solution is harder than a bare implementation. + * Desired EBNF support: + * * SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration | (AssociationPathExpression ["AS"] AliasIdentificationVariable) * + * It demands that entire SQL generation to become programmatical. This is + * needed because association based subselect requires "WHERE" conditional + * expressions to be injected, but there is no scope to do that. Only scope + * accessible is "FROM", prohibiting an easy implementation without larger + * changes.} + * * @return \Doctrine\ORM\Query\AST\SubselectIdentificationVariableDeclaration | * \Doctrine\ORM\Query\AST\IdentificationVariableDeclaration */ public function SubselectIdentificationVariableDeclaration() { - $this->lexer->glimpse(); + /* + NOT YET IMPLEMENTED! - /* NOT YET IMPLEMENTED! + $glimpse = $this->lexer->glimpse(); if ($glimpse['type'] == Lexer::T_DOT) { - $subselectIdVarDecl = new AST\SubselectIdentificationVariableDeclaration(); - $subselectIdVarDecl->associationPathExpression = $this->AssociationPathExpression(); - $this->match(Lexer::T_AS); - $subselectIdVarDecl->aliasIdentificationVariable = $this->AliasIdentificationVariable(); + $associationPathExpression = $this->AssociationPathExpression(); - return $subselectIdVarDecl; + if ($this->lexer->isNextToken(Lexer::T_AS)) { + $this->match(Lexer::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 + ); } */ @@ -1819,7 +1855,7 @@ class Parser { $token = $this->lexer->lookahead; $peek = $this->lexer->glimpse(); - + if ($token['type'] === Lexer::T_OPEN_PARENTHESIS && $peek['type'] === Lexer::T_SELECT) { $this->match(Lexer::T_OPEN_PARENTHESIS); $expression = $this->Subselect(); @@ -1827,7 +1863,7 @@ class Parser return $expression; } - + return $this->ScalarExpression(); } @@ -2097,7 +2133,7 @@ class Parser /** * SelectExpression ::= ( * IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | - * PartialObjectExpression | "(" Subselect ")" | CaseExpression + * PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression * ) [["AS"] ["HIDDEN"] AliasResultVariable] * * @return \Doctrine\ORM\Query\AST\SelectExpression @@ -3160,7 +3196,7 @@ class Parser case $this->lexer->isNextToken(Lexer::T_COALESCE): $expr = $this->CoalesceExpression(); break; - + case $this->isAggregateFunction($this->lexer->lookahead['type']): $expr = $this->AggregateExpression(); break; @@ -3346,7 +3382,10 @@ class Parser * "ABS" "(" SimpleArithmeticExpression ")" | * "SQRT" "(" SimpleArithmeticExpression ")" | * "MOD" "(" SimpleArithmeticExpression "," SimpleArithmeticExpression ")" | - * "SIZE" "(" CollectionValuedPathExpression ")" + * "SIZE" "(" CollectionValuedPathExpression ")" | + * "DATE_DIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | + * "BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" | + * "BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")" * * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode */ @@ -3377,7 +3416,12 @@ class Parser } /** - * FunctionsReturningDateTime ::= "CURRENT_DATE" | "CURRENT_TIME" | "CURRENT_TIMESTAMP" + * FunctionsReturningDateTime ::= + * "CURRENT_DATE" | + * "CURRENT_TIME" | + * "CURRENT_TIMESTAMP" | + * "DATE_ADD" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" | + * "DATE_SUB" "(" ArithmeticPrimary "," ArithmeticPrimary "," StringPrimary ")" * * @return \Doctrine\ORM\Query\AST\Functions\FunctionNode */ diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 8440bc176..7164f441f 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -803,32 +803,56 @@ class SqlWalker implements TreeWalker $sqlParts = array(); foreach ($identificationVarDecls as $identificationVariableDecl) { - $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); - - foreach ($identificationVariableDecl->joins as $join) { - $sql .= $this->walkJoin($join); - } - - if ($identificationVariableDecl->indexBy) { - $alias = $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable; - $field = $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field; - - if (isset($this->scalarFields[$alias][$field])) { - $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); - } else { - $this->rsm->addIndexBy( - $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, - $identificationVariableDecl->indexBy->simpleStateFieldPathExpression->field - ); - } - } - - $sqlParts[] = $sql; + $sqlParts[] = $this->walkIdentificationVariableDeclaration($identificationVariableDecl); } return ' FROM ' . implode(', ', $sqlParts); } + /** + * Walks down a IdentificationVariableDeclaration AST node, thereby generating the appropriate SQL. + * + * @param AST\IdentificationVariableDeclaration $identificationVariableDecl + * + * @return string + */ + public function walkIdentificationVariableDeclaration($identificationVariableDecl) + { + $sql = $this->walkRangeVariableDeclaration($identificationVariableDecl->rangeVariableDeclaration); + + if ($identificationVariableDecl->indexBy) { + $this->walkIndexBy($identificationVariableDecl->indexBy); + } + + foreach ($identificationVariableDecl->joins as $join) { + $sql .= $this->walkJoin($join); + } + + return $sql; + } + + /** + * Walks down a IndexBy AST node. + * + * @param AST\IndexBy $indexBy + * + * @return void + */ + public function walkIndexBy($indexBy) + { + $pathExpression = $indexBy->simpleStateFieldPathExpression; + $alias = $pathExpression->identificationVariable; + $field = $pathExpression->field; + + if (isset($this->scalarFields[$alias][$field])) { + $this->rsm->addIndexByScalar($this->scalarFields[$alias][$field]); + + return; + } + + $this->rsm->addIndexBy($alias, $field); + } + /** * Walks down a RangeVariableDeclaration AST node, thereby generating the appropriate SQL. * @@ -1017,10 +1041,7 @@ class SqlWalker implements TreeWalker // Apply the indexes if ($indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. - $this->rsm->addIndexBy( - $indexBy->simpleStateFieldPathExpression->identificationVariable, - $indexBy->simpleStateFieldPathExpression->field - ); + $this->walkIndexBy($indexBy); } else if (isset($relation['indexBy'])) { $this->rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); } @@ -1456,16 +1477,10 @@ class SqlWalker implements TreeWalker public function walkSubselectFromClause($subselectFromClause) { $identificationVarDecls = $subselectFromClause->identificationVariableDeclarations; - $sqlParts = array (); + $sqlParts = array (); foreach ($identificationVarDecls as $subselectIdVarDecl) { - $sql = $this->walkRangeVariableDeclaration($subselectIdVarDecl->rangeVariableDeclaration); - - foreach ($subselectIdVarDecl->joins as $join) { - $sql .= $this->walkJoin($join); - } - - $sqlParts[] = $sql; + $sqlParts[] = $this->walkIdentificationVariableDeclaration($subselectIdVarDecl); } return ' FROM ' . implode(', ', $sqlParts); @@ -1520,7 +1535,7 @@ class SqlWalker implements TreeWalker $qComp = $this->queryComponents[$dqlAlias]; $class = $qComp['metadata']; $fieldType = $class->getTypeOfField($fieldName); - + $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; break; diff --git a/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersJoinTest.php b/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersJoinTest.php index acbdced3e..60545d4f9 100644 --- a/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersJoinTest.php +++ b/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersJoinTest.php @@ -74,37 +74,40 @@ class CustomTreeWalkerJoin extends Query\TreeWalkerAdapter public function walkSelectStatement(Query\AST\SelectStatement $selectStatement) { foreach ($selectStatement->fromClause->identificationVariableDeclarations as $identificationVariableDeclaration) { - if ($identificationVariableDeclaration->rangeVariableDeclaration->abstractSchemaName == 'Doctrine\Tests\Models\CMS\CmsUser') { - $identificationVariableDeclaration->joins[] = new Query\AST\Join( - Query\AST\Join::JOIN_TYPE_LEFT, - new Query\AST\JoinAssociationDeclaration( - new Query\AST\JoinAssociationPathExpression( - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable, - 'address' - ), - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - null - ) - ); - $selectStatement->selectClause->selectExpressions[] = - new Query\AST\SelectExpression( - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - null, - false - ); - $meta1 = $this->_getQuery()->getEntityManager()->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); - $meta = $this->_getQuery()->getEntityManager()->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); - $this->setQueryComponent($identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - array( - 'metadata' => $meta, - 'parent' => $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable, - 'relation' => $meta1->getAssociationMapping('address'), - 'map' => null, - 'nestingLevel' => 0, - 'token' => null - ) - ); + $rangeVariableDecl = $identificationVariableDeclaration->rangeVariableDeclaration; + + if ($rangeVariableDecl->abstractSchemaName !== 'Doctrine\Tests\Models\CMS\CmsUser') { + continue; } + + $this->modifySelectStatement($selectStatement, $identificationVariableDeclaration); } } + + private function modifySelectStatement(Query\AST\SelectStatement $selectStatement, $identificationVariableDecl) + { + $rangeVariableDecl = $identificationVariableDecl->rangeVariableDeclaration; + $joinAssocPathExpression = new Query\AST\JoinAssociationPathExpression($rangeVariableDecl->aliasIdentificationVariable, 'address'); + $joinAssocDeclaration = new Query\AST\JoinAssociationDeclaration($joinAssocPathExpression, $rangeVariableDecl->aliasIdentificationVariable . 'a', null); + $join = new Query\AST\Join(Query\AST\Join::JOIN_TYPE_LEFT, $joinAssocDeclaration); + $selectExpression = new Query\AST\SelectExpression($rangeVariableDecl->aliasIdentificationVariable . 'a', null, false); + + $identificationVariableDecl->joins[] = $join; + $selectStatement->selectClause->selectExpressions[] = $selectExpression; + + $entityManager = $this->_getQuery()->getEntityManager(); + $userMetadata = $entityManager->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $addressMetadata = $entityManager->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); + + $this->setQueryComponent($rangeVariableDecl->aliasIdentificationVariable . 'a', + array( + 'metadata' => $addressMetadata, + 'parent' => $rangeVariableDecl->aliasIdentificationVariable, + 'relation' => $userMetadata->getAssociationMapping('address'), + 'map' => null, + 'nestingLevel' => 0, + 'token' => null, + ) + ); + } } diff --git a/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersTest.php b/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersTest.php index f0dbbe8ef..574b37ae7 100644 --- a/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersTest.php +++ b/tests/Doctrine/Tests/ORM/Query/CustomTreeWalkersTest.php @@ -198,37 +198,40 @@ class CustomTreeWalkerJoin extends Query\TreeWalkerAdapter public function walkSelectStatement(Query\AST\SelectStatement $selectStatement) { foreach ($selectStatement->fromClause->identificationVariableDeclarations as $identificationVariableDeclaration) { - if ($identificationVariableDeclaration->rangeVariableDeclaration->abstractSchemaName == 'Doctrine\Tests\Models\CMS\CmsUser') { - $identificationVariableDeclaration->joins[] = new Query\AST\Join( - Query\AST\Join::JOIN_TYPE_LEFT, - new Query\AST\JoinAssociationDeclaration( - new Query\AST\JoinAssociationPathExpression( - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable, - 'address' - ), - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - null - ) - ); - $selectStatement->selectClause->selectExpressions[] = - new Query\AST\SelectExpression( - $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - null, - false - ); - $meta1 = $this->_getQuery()->getEntityManager()->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); - $meta = $this->_getQuery()->getEntityManager()->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); - $this->setQueryComponent($identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable . 'a', - array( - 'metadata' => $meta, - 'parent' => $identificationVariableDeclaration->rangeVariableDeclaration->aliasIdentificationVariable, - 'relation' => $meta1->getAssociationMapping('address'), - 'map' => null, - 'nestingLevel' => 0, - 'token' => null - ) - ); + $rangeVariableDecl = $identificationVariableDeclaration->rangeVariableDeclaration; + + if ($rangeVariableDecl->abstractSchemaName !== 'Doctrine\Tests\Models\CMS\CmsUser') { + continue; } + + $this->modifySelectStatement($selectStatement, $identificationVariableDeclaration); } } -} + + private function modifySelectStatement(Query\AST\SelectStatement $selectStatement, $identificationVariableDecl) + { + $rangeVariableDecl = $identificationVariableDecl->rangeVariableDeclaration; + $joinAssocPathExpression = new Query\AST\JoinAssociationPathExpression($rangeVariableDecl->aliasIdentificationVariable, 'address'); + $joinAssocDeclaration = new Query\AST\JoinAssociationDeclaration($joinAssocPathExpression, $rangeVariableDecl->aliasIdentificationVariable . 'a', null); + $join = new Query\AST\Join(Query\AST\Join::JOIN_TYPE_LEFT, $joinAssocDeclaration); + $selectExpression = new Query\AST\SelectExpression($rangeVariableDecl->aliasIdentificationVariable . 'a', null, false); + + $identificationVariableDecl->joins[] = $join; + $selectStatement->selectClause->selectExpressions[] = $selectExpression; + + $entityManager = $this->_getQuery()->getEntityManager(); + $userMetadata = $entityManager->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $addressMetadata = $entityManager->getClassMetadata('Doctrine\Tests\Models\CMS\CmsAddress'); + + $this->setQueryComponent($rangeVariableDecl->aliasIdentificationVariable . 'a', + array( + 'metadata' => $addressMetadata, + 'parent' => $rangeVariableDecl->aliasIdentificationVariable, + 'relation' => $userMetadata->getAssociationMapping('address'), + 'map' => null, + 'nestingLevel' => 0, + 'token' => null, + ) + ); + } +} \ No newline at end of file