From 6d9c4aacfce184be9c2661b68b5721c1cf11b428 Mon Sep 17 00:00:00 2001 From: eltharin Date: Fri, 21 Feb 2025 14:50:41 +0100 Subject: [PATCH] add capability to use allfields sql notation in a dto, this PR allow to call u.* to get all fileds fo u entity in one call, --- .../reference/dql-doctrine-query-language.rst | 14 +- src/Query/AST/AllFieldsExpression.php | 27 ++ src/Query/AST/NewObjectExpression.php | 15 +- src/Query/Parser.php | 44 ++- src/Query/SqlWalker.php | 71 +++- tests/Tests/Models/CMS/CmsDumbVariadicDTO.php | 22 ++ .../Tests/ORM/Functional/NewOperatorTest.php | 341 ++++++++++++++++++ 7 files changed, 510 insertions(+), 24 deletions(-) create mode 100644 src/Query/AST/AllFieldsExpression.php create mode 100644 tests/Tests/Models/CMS/CmsDumbVariadicDTO.php diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index e668c08fd82..07862c1ce40 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -674,6 +674,17 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate : If two arguments have the same name, a ``DuplicateFieldException`` is thrown. If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them. +In a Dto, if you want add all fields of an entity, you can use AllFields notation : +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(a.*) AS address) FROM Customer c JOIN c.address a'); + $users = $query->getResult(); // array of CustomerDTO + + // CustomerDTO => {name : 'DOE', email: null, city: null, address: {id: 18, city: 'New York', zip: '10011'}} + +It's recommended to use named arguments Dto with AllFields notation because argument order is not guaranteed. + Using INDEX BY ~~~~~~~~~~~~~~ @@ -1702,7 +1713,8 @@ Select Expressions PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}" NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" - NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable] + NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression + AllFieldsExpression ::= IdentificationVariable ".*" Conditional Expressions ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/Query/AST/AllFieldsExpression.php b/src/Query/AST/AllFieldsExpression.php new file mode 100644 index 00000000000..b210a53f73e --- /dev/null +++ b/src/Query/AST/AllFieldsExpression.php @@ -0,0 +1,27 @@ +walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap); + } +} diff --git a/src/Query/AST/NewObjectExpression.php b/src/Query/AST/NewObjectExpression.php index 7383c487234..eef522155f1 100644 --- a/src/Query/AST/NewObjectExpression.php +++ b/src/Query/AST/NewObjectExpression.php @@ -6,6 +6,9 @@ use Doctrine\ORM\Query\SqlWalker; +use function func_get_arg; +use function func_num_args; + /** * NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")" * @@ -13,9 +16,17 @@ */ class NewObjectExpression extends Node { - /** @param mixed[] $args */ - public function __construct(public string $className, public array $args) + public bool $hasNamedArgs = false; + + /** + * @param class-string $className + * @param mixed[] $args + */ + public function __construct(public string $className, public array $args /*, public bool $hasNamedArgs = false */) { + if (func_num_args() > 2) { + $this->hasNamedArgs = func_get_arg(2); + } } public function dispatch(SqlWalker $walker): string diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 86fb8243b49..0c3dd47e9ea 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -1036,14 +1036,20 @@ public function PathExpression(int $expectedTypes): AST\PathExpression assert($this->lexer->token !== null); if ($this->lexer->isNextToken(TokenType::T_DOT)) { $this->match(TokenType::T_DOT); - $this->match(TokenType::T_IDENTIFIER); - - $field = $this->lexer->token->value; - while ($this->lexer->isNextToken(TokenType::T_DOT)) { - $this->match(TokenType::T_DOT); + if ($this->lexer->isNextToken(TokenType::T_MULTIPLY)) { + $this->match(TokenType::T_MULTIPLY); + $field = $this->lexer->token->value; + } else { $this->match(TokenType::T_IDENTIFIER); - $field .= '.' . $this->lexer->token->value; + + $field = $this->lexer->token->value; + + while ($this->lexer->isNextToken(TokenType::T_DOT)) { + $this->match(TokenType::T_DOT); + $this->match(TokenType::T_IDENTIFIER); + $field .= '.' . $this->lexer->token->value; + } } } @@ -1106,6 +1112,20 @@ public function CollectionValuedPathExpression(): AST\PathExpression return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION); } + /** + * AllFieldsExpression ::= IdentificationVariable + */ + public function AllFieldsExpression(): AST\AllFieldsExpression + { + $identVariable = $this->IdentificationVariable(); + assert($this->lexer->token !== null); + + $this->match(TokenType::T_DOT); + $this->match(TokenType::T_MULTIPLY); + + return new AST\AllFieldsExpression($identVariable); + } + /** * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} */ @@ -1781,7 +1801,7 @@ public function NewObjectExpression(): AST\NewObjectExpression $this->match(TokenType::T_CLOSE_PARENTHESIS); - $expression = new AST\NewObjectExpression($className, $args); + $expression = new AST\NewObjectExpression($className, $args, $useNamedArguments); // Defer NewObjectExpression validation $this->deferredNewObjectExpressions[] = [ @@ -1828,7 +1848,7 @@ public function addArgument(array &$args, bool $useNamedArguments): void } /** - * NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable] + * NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression */ public function NewObjectArg(string|null &$fieldAlias = null): mixed { @@ -1934,10 +1954,14 @@ public function ScalarExpression(): mixed // it is no function, so it must be a field path case $lookahead === TokenType::T_IDENTIFIER: $this->lexer->peek(); // lookahead => '.' - $this->lexer->peek(); // lookahead => token after '.' - $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' + $token = $this->lexer->peek(); // lookahead => token after '.' + $peek = $this->lexer->peek(); // lookahead => token after the token after the '.' $this->lexer->resetPeek(); + if ($token->value === '*') { + return $this->AllFieldsExpression(); + } + if ($this->isMathOperator($peek)) { return $this->SimpleArithmeticExpression(); } diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 9089995e432..468895c7f99 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1507,7 +1507,8 @@ public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesis public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string { $sqlSelectExpressions = []; - $objOwner = $objOwnerIdx = null; + $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; + $aliasGap = $newObjectExpression->hasNamedArgs ? null : 0; if ($this->newObjectStack !== []) { [$objOwner, $objOwnerIdx] = end($this->newObjectStack); @@ -1517,9 +1518,14 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri } foreach ($newObjectExpression->args as $argIndex => $e) { - $resultAlias = $this->scalarResultCounter++; - $columnAlias = $this->getSQLColumnAlias('sclr'); - $fieldType = 'string'; + if (! $newObjectExpression->hasNamedArgs) { + $argIndex += $aliasGap; + } + + $resultAlias = $this->scalarResultCounter++; + $columnAlias = $this->getSQLColumnAlias('sclr'); + $fieldType = 'string'; + $isScalarResult = true; switch (true) { case $e instanceof AST\NewObjectExpression: @@ -1567,19 +1573,26 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; break; + case $e instanceof AST\AllFieldsExpression: + $isScalarResult = false; + $sqlSelectExpressions[] = $e->dispatch($this, $objIndex, $argIndex, $aliasGap); + break; + default: $sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias; break; } - $this->scalarResultAliasMap[$resultAlias] = $columnAlias; - $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); + if ($isScalarResult) { + $this->scalarResultAliasMap[$resultAlias] = $columnAlias; + $this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType); - $this->rsm->newObjectMappings[$columnAlias] = [ - 'className' => $newObjectExpression->className, - 'objIndex' => $objIndex, - 'argIndex' => $argIndex, - ]; + $this->rsm->newObjectMappings[$columnAlias] = [ + 'className' => $newObjectExpression->className, + 'objIndex' => $objIndex, + 'argIndex' => $argIndex, + ]; + } } return implode(', ', $sqlSelectExpressions); @@ -2282,6 +2295,42 @@ public function walkResultVariable(string $resultVariable): string return $resultAlias; } + public function walkAllEntityFieldsExpression(AST\AllFieldsExpression $expression, int|string $objIndex, int|string $argIndex, int|null &$aliasGap): string + { + $dqlAlias = $expression->identificationVariable; + $class = $this->getMetadataForDqlAlias($expression->identificationVariable); + + $sqlParts = []; + // Select all fields from the queried class + foreach ($class->fieldMappings as $fieldName => $mapping) { + $tableName = isset($mapping->inherited) + ? $this->em->getClassMetadata($mapping->inherited)->getTableName() + : $class->getTableName(); + + $sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias); + $columnAlias = $this->getSQLColumnAlias($mapping->columnName); + $quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + + $col = $sqlTableAlias . '.' . $quotedColumnName; + + $type = Type::getType($mapping->type); + $col = $type->convertToPHPValueSQL($col, $this->platform); + + $sqlParts[] = $col . ' AS ' . $columnAlias; + + $this->scalarResultAliasMap[$objIndex][] = $columnAlias; + + $this->rsm->addScalarResult($columnAlias, $objIndex, $mapping->type); + + $this->rsm->newObjectMappings[$columnAlias] = [ + 'objIndex' => $objIndex, + 'argIndex' => $aliasGap === null ? $fieldName : $argIndex + $aliasGap++, + ]; + } + + return implode(', ', $sqlParts); + } + /** * @return string The list in parentheses of valid child discriminators from the given class * diff --git a/tests/Tests/Models/CMS/CmsDumbVariadicDTO.php b/tests/Tests/Models/CMS/CmsDumbVariadicDTO.php new file mode 100644 index 00000000000..192da6df7d4 --- /dev/null +++ b/tests/Tests/Models/CMS/CmsDumbVariadicDTO.php @@ -0,0 +1,22 @@ + $val) { + $this->values[$key] = $val; + } + } + + public function __get(string $key): mixed + { + return $this->values[$key] ?? null; + } +} diff --git a/tests/Tests/ORM/Functional/NewOperatorTest.php b/tests/Tests/ORM/Functional/NewOperatorTest.php index 2394b6fd880..959164d5421 100644 --- a/tests/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Tests/ORM/Functional/NewOperatorTest.php @@ -11,6 +11,8 @@ use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsAddressDTO; use Doctrine\Tests\Models\CMS\CmsAddressDTONamedArgs; +use Doctrine\Tests\Models\CMS\CmsDumbDTO; +use Doctrine\Tests\Models\CMS\CmsDumbVariadicDTO; use Doctrine\Tests\Models\CMS\CmsEmail; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsUser; @@ -1159,6 +1161,345 @@ public function testNamedArguments(): void self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']); } + public function testShouldSupportNestedNewOperatorsWithNestedDtoNotLast(): void + { + $dql = ' + SELECT + new CmsUserDTO( + u.name, + e.email, + new CmsAddressDTO( + a.country, + a.city, + a.zip, + \'Abbey Road\', + new CmsDumbDTO( + a.country, + a.city, + new CmsDumbDTO( + a.zip, + 456 + ), + a.zip + ) + ), + 555812452 + ) as user, + u.status, + u.username as cmsUserUsername + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTO::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTO::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTO::class, $result[2]['user']); + + self::assertSame($this->fixtures[0]->name, $result[0]['user']->name); + self::assertSame($this->fixtures[1]->name, $result[1]['user']->name); + self::assertSame($this->fixtures[2]->name, $result[2]['user']->name); + + self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email); + self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email); + self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email); + + self::assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->address); + self::assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->address); + self::assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->address); + + self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->address->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->address->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->address->country); + + self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->address->city); + self::assertSame($this->fixtures[0]->address->city, $result[0]['user']->address->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->address->city); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]['user']->address->other); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]['user']->address->other); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]['user']->address->other); + + self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->address->other->val1); + self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->address->other->val1); + self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->address->other->val1); + + self::assertSame($this->fixtures[0]->address->city, $result[0]['user']->address->other->val2); + self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->address->other->val2); + self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->address->other->val2); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]['user']->address->other->val3); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]['user']->address->other->val3); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]['user']->address->other->val3); + + self::assertSame($this->fixtures[0]->address->zip, $result[0]['user']->address->other->val3->val1); + self::assertSame($this->fixtures[1]->address->zip, $result[1]['user']->address->other->val3->val1); + self::assertSame($this->fixtures[2]->address->zip, $result[2]['user']->address->other->val3->val1); + + self::assertSame(456, $result[0]['user']->address->other->val3->val2); + self::assertSame(456, $result[1]['user']->address->other->val3->val2); + self::assertSame(456, $result[2]['user']->address->other->val3->val2); + + self::assertSame($this->fixtures[0]->address->zip, $result[0]['user']->address->other->val4); + self::assertSame($this->fixtures[1]->address->zip, $result[1]['user']->address->other->val4); + self::assertSame($this->fixtures[2]->address->zip, $result[2]['user']->address->other->val4); + + self::assertSame(555812452, $result[0]['user']->phonenumbers); + self::assertSame(555812452, $result[1]['user']->phonenumbers); + self::assertSame(555812452, $result[2]['user']->phonenumbers); + + self::assertSame($this->fixtures[0]->status, $result[0]['status']); + self::assertSame($this->fixtures[1]->status, $result[1]['status']); + self::assertSame($this->fixtures[2]->status, $result[2]['status']); + + self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']); + self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']); + self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForDto(): void + { + $dql = ' + SELECT + new CmsDumbDTO( + u.* + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]); + + self::assertSame($this->fixtures[0]->status, $result[0]->val2); + self::assertSame($this->fixtures[1]->status, $result[1]->val2); + self::assertSame($this->fixtures[2]->status, $result[2]->val2); + + self::assertSame($this->fixtures[0]->username, $result[0]->val3); + self::assertSame($this->fixtures[1]->username, $result[1]->val3); + self::assertSame($this->fixtures[2]->username, $result[2]->val3); + + self::assertSame($this->fixtures[0]->name, $result[0]->val4); + self::assertSame($this->fixtures[1]->name, $result[1]->val4); + self::assertSame($this->fixtures[2]->name, $result[2]->val4); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNamedDto(): void + { + $dql = ' + SELECT + new NAMED CmsDumbVariadicDTO( + u.* + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[0]); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[1]); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[2]); + + self::assertSame($this->fixtures[0]->status, $result[0]->status); + self::assertSame($this->fixtures[1]->status, $result[1]->status); + self::assertSame($this->fixtures[2]->status, $result[2]->status); + + self::assertSame($this->fixtures[0]->username, $result[0]->username); + self::assertSame($this->fixtures[1]->username, $result[1]->username); + self::assertSame($this->fixtures[2]->username, $result[2]->username); + + self::assertSame($this->fixtures[0]->name, $result[0]->name); + self::assertSame($this->fixtures[1]->name, $result[1]->name); + self::assertSame($this->fixtures[2]->name, $result[2]->name); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNamedDtoWithOtherValues(): void + { + $dql = ' + SELECT + new NAMED CmsDumbVariadicDTO( + u.*, e.email, a.zip, a.country + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[0]); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[1]); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[2]); + + self::assertSame($this->fixtures[0]->status, $result[0]->status); + self::assertSame($this->fixtures[1]->status, $result[1]->status); + self::assertSame($this->fixtures[2]->status, $result[2]->status); + + self::assertSame($this->fixtures[0]->username, $result[0]->username); + self::assertSame($this->fixtures[1]->username, $result[1]->username); + self::assertSame($this->fixtures[2]->username, $result[2]->username); + + self::assertSame($this->fixtures[0]->name, $result[0]->name); + self::assertSame($this->fixtures[1]->name, $result[1]->name); + self::assertSame($this->fixtures[2]->name, $result[2]->name); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->email); + self::assertSame($this->fixtures[1]->email->email, $result[1]->email); + self::assertSame($this->fixtures[2]->email->email, $result[2]->email); + + self::assertSame($this->fixtures[0]->address->zip, $result[0]->zip); + self::assertSame($this->fixtures[1]->address->zip, $result[1]->zip); + self::assertSame($this->fixtures[2]->address->zip, $result[2]->zip); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]->country); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNestedDto(): void + { + $dql = ' + SELECT + new CmsDumbDTO( + u.name, + e.email, + new CmsDumbDTO( + a.* + ) as address + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]); + + self::assertSame($this->fixtures[0]->name, $result[0]->val1); + self::assertSame($this->fixtures[1]->name, $result[1]->val1); + self::assertSame($this->fixtures[2]->name, $result[2]->val1); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->val2); + self::assertSame($this->fixtures[1]->email->email, $result[1]->val2); + self::assertSame($this->fixtures[2]->email->email, $result[2]->val2); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]->val3); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]->val3); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]->val3); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->val3->val2); + self::assertSame($this->fixtures[1]->address->country, $result[1]->val3->val2); + self::assertSame($this->fixtures[2]->address->country, $result[2]->val3->val2); + + self::assertSame($this->fixtures[0]->address->zip, $result[0]->val3->val3); + self::assertSame($this->fixtures[1]->address->zip, $result[1]->val3->val3); + self::assertSame($this->fixtures[2]->address->zip, $result[2]->val3->val3); + + self::assertSame($this->fixtures[0]->address->city, $result[0]->val3->val4); + self::assertSame($this->fixtures[1]->address->city, $result[1]->val3->val4); + self::assertSame($this->fixtures[2]->address->city, $result[2]->val3->val4); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNestedNamedDto(): void + { + $dql = ' + SELECT + new CmsDumbDTO( + u.name, + e.email, + new NAMED CmsDumbVariadicDTO( + a.* + ) as address + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e + JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]); + + self::assertSame($this->fixtures[0]->name, $result[0]->val1); + self::assertSame($this->fixtures[1]->name, $result[1]->val1); + self::assertSame($this->fixtures[2]->name, $result[2]->val1); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->val2); + self::assertSame($this->fixtures[1]->email->email, $result[1]->val2); + self::assertSame($this->fixtures[2]->email->email, $result[2]->val2); + + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[0]->val3); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[1]->val3); + self::assertInstanceOf(CmsDumbVariadicDTO::class, $result[2]->val3); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->val3->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]->val3->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]->val3->country); + + self::assertSame($this->fixtures[2]->address->city, $result[2]->val3->city); + self::assertSame($this->fixtures[0]->address->city, $result[0]->val3->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]->val3->city); + + self::assertSame($this->fixtures[2]->address->zip, $result[2]->val3->zip); + self::assertSame($this->fixtures[0]->address->zip, $result[0]->val3->zip); + self::assertSame($this->fixtures[1]->address->zip, $result[1]->val3->zip); + } + public function testVariadicArgument(): void { $dql = <<<'SQL'