From 2643d1dbf2fb6a63b2f01e1580dcc39ccb970a84 Mon Sep 17 00:00:00 2001 From: eltharin Date: Sat, 21 Jun 2025 13:12:56 +0200 Subject: [PATCH] replace all fileds fonctionnality replace u.* by ALLFIELDS(u) and add an exception when not use named arguments dto add enum properties too --- .../reference/dql-doctrine-query-language.rst | 19 +- phpstan-baseline.neon | 2 +- src/Query/AST/AllFieldsExpression.php | 28 ++ src/Query/AST/NewObjectExpression.php | 2 +- src/Query/Parser.php | 25 +- src/Query/SqlWalker.php | 79 +++++- src/Query/TokenType.php | 117 ++++----- tests/Tests/Models/CMS/CmsDumbVariadicDTO.php | 22 ++ .../Tests/ORM/Functional/NewOperatorTest.php | 239 ++++++++++++++++++ 9 files changed, 457 insertions(+), 76 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 9b0b0539ddd..46d20366406 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -684,6 +684,18 @@ You can hydrate an entity nested in a DTO : // CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'} +In a DTO, if you want to add all fields of an entity, you can use ``ALLFIELDS`` : + +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(ALLFIELDS(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 mandatory to use named arguments DTOs with the ``ALLFIELDS`` notation because argument order is not guaranteed, otherwise an exception will be thrown. + Using INDEX BY ~~~~~~~~~~~~~~ @@ -1707,13 +1719,14 @@ Select Expressions .. code-block:: php - SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable] - SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] 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" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")" - NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable] + NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]) | AllFieldsExpression EntityAsDtoArgumentExpression ::= IdentificationVariable + AllFieldsExpression ::= "ALLFIELDS(" IdentificationVariable ")" Conditional Expressions ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ceebcbe0485..8c78ca00e5f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2811,7 +2811,7 @@ parameters: - message: '#^Cannot assign new offset to list\\|string\.$#' identifier: offsetAssign.dimType - count: 2 + count: 3 path: src/Query/SqlWalker.php - diff --git a/src/Query/AST/AllFieldsExpression.php b/src/Query/AST/AllFieldsExpression.php new file mode 100644 index 00000000000..b63a9f816ec --- /dev/null +++ b/src/Query/AST/AllFieldsExpression.php @@ -0,0 +1,28 @@ +field = $this->identificationVariable . '.*'; + } + + public function dispatch(SqlWalker $walker, int|string $parent = '', int|string $argIndex = '', int|null &$aliasGap = null): string + { + return $walker->walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap); + } +} diff --git a/src/Query/AST/NewObjectExpression.php b/src/Query/AST/NewObjectExpression.php index 1d46722676e..91b0e7ea5ad 100644 --- a/src/Query/AST/NewObjectExpression.php +++ b/src/Query/AST/NewObjectExpression.php @@ -20,7 +20,7 @@ class NewObjectExpression extends Node * @param class-string $className * @param mixed[] $args */ - public function __construct(public string $className, public array $args) + public function __construct(public string $className, public array $args, public bool $hasNamedArgs = false) { } diff --git a/src/Query/Parser.php b/src/Query/Parser.php index daf282c8b70..457d1c80dfb 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -1150,6 +1150,21 @@ public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpressi return new AST\EntityAsDtoArgumentExpression($expression, $identVariable); } + /** + * AllFieldsExpression ::= "ALLFIELDS(" IdentificationVariable ")" + */ + public function AllFieldsExpression(): AST\AllFieldsExpression + { + assert($this->lexer->token !== null); + + $this->match(TokenType::T_ALLFIELDS); + $this->match(TokenType::T_OPEN_PARENTHESIS); + $identVariable = $this->IdentificationVariable(); + $this->match(TokenType::T_CLOSE_PARENTHESIS); + + return new AST\AllFieldsExpression($identVariable); + } + /** * SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression} */ @@ -1826,7 +1841,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[] = [ @@ -1873,7 +1888,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 { @@ -1893,6 +1908,8 @@ public function NewObjectArg(string|null &$fieldAlias = null): mixed $this->match(TokenType::T_CLOSE_PARENTHESIS); } elseif ($token->type === TokenType::T_NEW) { $expression = $this->NewObjectExpression(); + } elseif ($token->type === TokenType::T_ALLFIELDS) { + $expression = $this->AllFieldsExpression(); } elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) { $expression = $this->EntityAsDtoArgumentExpression(); } else { @@ -1985,8 +2002,8 @@ 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 ($this->isMathOperator($peek)) { diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index a943deb050f..c7335e4c1ac 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1352,6 +1352,9 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st $sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable); break; + case $expr instanceof AST\AllFieldsExpression: + throw new LogicException('All fields Expression are only supported in DTO.'); + default: // IdentificationVariable or PartialObjectExpression if ($expr instanceof AST\PartialObjectExpression) { @@ -1518,11 +1521,17 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri { $sqlSelectExpressions = []; $objIndex = $newObjectResultAlias ?: $this->newObjectCounter++; + $aliasGap = $newObjectExpression->hasNamedArgs ? null : 0; 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: @@ -1576,18 +1585,30 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri $sqlSelectExpressions[] = trim($e->dispatch($this)); break; + case $e instanceof AST\AllFieldsExpression: + if (! $newObjectExpression->hasNamedArgs) { + throw new LogicException('All fields Expression must be used with named arguments DTO constructor.'); + } + + $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] = [ - 'objIndex' => $objIndex, - 'argIndex' => $argIndex, - ]; + $this->rsm->newObjectMappings[$columnAlias] = [ + 'className' => $newObjectExpression->className, + 'objIndex' => $objIndex, + 'argIndex' => $argIndex, + ]; + } } $this->rsm->newObject[$objIndex] = $newObjectExpression->className; @@ -2292,6 +2313,46 @@ 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); + + if ($mapping->enumType !== null) { + $this->rsm->addEnumResult($columnAlias, $mapping->enumType); + } + + $this->rsm->newObjectMappings[$columnAlias] = [ + 'objIndex' => $objIndex, + 'argIndex' => $aliasGap === null ? $fieldName : (int) $argIndex + $aliasGap++, + ]; + } + + return implode(', ', $sqlParts); + } + /** * @return string The list in parentheses of valid child discriminators from the given class * diff --git a/src/Query/TokenType.php b/src/Query/TokenType.php index 47cc7912711..077b3fbae44 100644 --- a/src/Query/TokenType.php +++ b/src/Query/TokenType.php @@ -32,62 +32,63 @@ enum TokenType: int case T_IDENTIFIER = 102; // All keyword tokens should be >= 200 - case T_ALL = 200; - case T_AND = 201; - case T_ANY = 202; - case T_AS = 203; - case T_ASC = 204; - case T_AVG = 205; - case T_BETWEEN = 206; - case T_BOTH = 207; - case T_BY = 208; - case T_CASE = 209; - case T_COALESCE = 210; - case T_COUNT = 211; - case T_DELETE = 212; - case T_DESC = 213; - case T_DISTINCT = 214; - case T_ELSE = 215; - case T_EMPTY = 216; - case T_END = 217; - case T_ESCAPE = 218; - case T_EXISTS = 219; - case T_FALSE = 220; - case T_FROM = 221; - case T_GROUP = 222; - case T_HAVING = 223; - case T_HIDDEN = 224; - case T_IN = 225; - case T_INDEX = 226; - case T_INNER = 227; - case T_INSTANCE = 228; - case T_IS = 229; - case T_JOIN = 230; - case T_LEADING = 231; - case T_LEFT = 232; - case T_LIKE = 233; - case T_MAX = 234; - case T_MEMBER = 235; - case T_MIN = 236; - case T_NEW = 237; - case T_NOT = 238; - case T_NULL = 239; - case T_NULLIF = 240; - case T_OF = 241; - case T_OR = 242; - case T_ORDER = 243; - case T_OUTER = 244; - case T_PARTIAL = 245; - case T_SELECT = 246; - case T_SET = 247; - case T_SOME = 248; - case T_SUM = 249; - case T_THEN = 250; - case T_TRAILING = 251; - case T_TRUE = 252; - case T_UPDATE = 253; - case T_WHEN = 254; - case T_WHERE = 255; - case T_WITH = 256; - case T_NAMED = 257; + case T_ALL = 200; + case T_AND = 201; + case T_ANY = 202; + case T_AS = 203; + case T_ASC = 204; + case T_AVG = 205; + case T_BETWEEN = 206; + case T_BOTH = 207; + case T_BY = 208; + case T_CASE = 209; + case T_COALESCE = 210; + case T_COUNT = 211; + case T_DELETE = 212; + case T_DESC = 213; + case T_DISTINCT = 214; + case T_ELSE = 215; + case T_EMPTY = 216; + case T_END = 217; + case T_ESCAPE = 218; + case T_EXISTS = 219; + case T_FALSE = 220; + case T_FROM = 221; + case T_GROUP = 222; + case T_HAVING = 223; + case T_HIDDEN = 224; + case T_IN = 225; + case T_INDEX = 226; + case T_INNER = 227; + case T_INSTANCE = 228; + case T_IS = 229; + case T_JOIN = 230; + case T_LEADING = 231; + case T_LEFT = 232; + case T_LIKE = 233; + case T_MAX = 234; + case T_MEMBER = 235; + case T_MIN = 236; + case T_NEW = 237; + case T_NOT = 238; + case T_NULL = 239; + case T_NULLIF = 240; + case T_OF = 241; + case T_OR = 242; + case T_ORDER = 243; + case T_OUTER = 244; + case T_PARTIAL = 245; + case T_SELECT = 246; + case T_SET = 247; + case T_SOME = 248; + case T_SUM = 249; + case T_THEN = 250; + case T_TRAILING = 251; + case T_TRUE = 252; + case T_UPDATE = 253; + case T_WHEN = 254; + case T_WHERE = 255; + case T_WITH = 256; + case T_NAMED = 257; + case T_ALLFIELDS = 258; } 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 d133dc7e06d..bd5b509fdf5 100644 --- a/tests/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Tests/ORM/Functional/NewOperatorTest.php @@ -12,6 +12,7 @@ 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; @@ -1571,6 +1572,244 @@ public function testShouldSupportNestedNewOperatorsWithNestedDtoNotLast(): void self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']); } + public function testShouldSupportNestedNewOperatorsWithAllFieldsForDto(): void + { + $dql = ' + SELECT + new CmsDumbDTO( + ALLFIELDS(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); + $this->expectExceptionMessage('All fields Expression must be used with named arguments DTO constructor.'); + $result = $query->getResult(); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNamedDto(): void + { + $dql = ' + SELECT + new NAMED CmsDumbVariadicDTO( + ALLFIELDS(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 testShouldSupportNestedNewOperatorsWithMultipleAllFieldsForNamedDto(): void + { + $dql = ' + SELECT + new NAMED CmsDumbVariadicDTO( + ALLFIELDS(u), ALLFIELDS(a) + ) + 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]->address->city, $result[0]->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]->city); + + 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 testShouldSupportNestedNewOperatorsWithAllFieldsForNamedDtoWithOtherValues(): void + { + $dql = ' + SELECT + new NAMED CmsDumbVariadicDTO( + ALLFIELDS(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( + ALLFIELDS(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); + $this->expectExceptionMessage('All fields Expression must be used with named arguments DTO constructor.'); + $result = $query->getResult(); + } + + public function testShouldSupportNestedNewOperatorsWithAllFieldsForNestedNamedDto(): void + { + $dql = ' + SELECT + new CmsDumbDTO( + u.name, + e.email, + new NAMED CmsDumbVariadicDTO( + ALLFIELDS(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'