diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index c2b31cd326d..749ce3cadb4 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -591,7 +591,7 @@ You can also nest several DTO : // Bind values to the object properties. } } - + class AddressDTO { public function __construct(string $street, string $city, string $zip) @@ -599,15 +599,77 @@ You can also nest several DTO : // Bind values to the object properties. } } - + .. code-block:: php createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a'); $users = $query->getResult(); // array of CustomerDTO - + Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor. +If you use your data-transfer objects for multiple queries but not necessarily with the same parameters, +you can use named arguments or variadic arguments, add ``named`` keyword in your DQL querie before your Dto : + +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a'); + $users = $query->getResult(); // array of CustomerDTO + + // CustomerDTO => {name : 'SMITH', email: null, city: 'London', value: null} + +ORM will also look column aliases before columns names : + +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a'); + $users = $query->getResult(); // array of CustomerDTO + + // CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'} + +For aliases a field you can use SQL notation with `AS` keyword, or the PHP named arguments notation : + +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(name: c.name, value: CONCAT(a.city, ' ' , a.zip)) FROM Customer c JOIN c.address a'); + $users = $query->getResult(); // array of CustomerDTO + + // CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'} + +The ``NAMED`` keyword must be added to all dto you want to instantiate : + +.. code-block:: php + + createQuery('SELECT NEW NAMED CustomerDTO(name: c.name, address: NEW NAMED AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.address a'); + $users = $query->getResult(); // array of CustomerDTO + + // CustomerDTO => {name : 'DOE', email: null, city: null, value: 'New York 10011'} + +If two fields have the same name/alias, a RepeatedAliasException is thrown. +If a field without column name (functions for example) has no alias set, a MissingAliasException is thrown. + Using INDEX BY ~~~~~~~~~~~~~~ diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b83ae43a889..8625d325cea 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -961,6 +961,7 @@ value]]> value]]> lexer->glimpse()->type]]> + type]]> value]]> value]]> diff --git a/src/Exception/MissingAliasException.php b/src/Exception/MissingAliasException.php new file mode 100644 index 00000000000..e6a38031b33 --- /dev/null +++ b/src/Exception/MissingAliasException.php @@ -0,0 +1,15 @@ + []]; + $rowData = ['data' => [], 'newObjects' => []]; foreach ($data as $key => $value) { $cacheKeyInfo = $this->hydrateColumnInfo($key); @@ -282,10 +282,6 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); } - if (! isset($rowData['newObjects'])) { - $rowData['newObjects'] = []; - } - $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class']; $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; break; @@ -341,28 +337,22 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon } foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) { - if (! isset($rowData['newObjects'][$objIndex])) { + if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) { continue; } - $newObject = $rowData['newObjects'][$objIndex]; - unset($rowData['newObjects'][$objIndex]); + $newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex]; + unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]); - $class = $newObject['class']; - $args = $newObject['args']; - $obj = $class->newInstanceArgs($args); + $obj = $newObject['class']->newInstanceArgs($newObject['args']); $rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj; } - if (isset($rowData['newObjects'])) { - foreach ($rowData['newObjects'] as $objIndex => $newObject) { - $class = $newObject['class']; - $args = $newObject['args']; - $obj = $class->newInstanceArgs($args); + foreach ($rowData['newObjects'] as $objIndex => $newObject) { + $obj = $newObject['class']->newInstanceArgs($newObject['args']); - $rowData['newObjects'][$objIndex]['obj'] = $obj; - } + $rowData['newObjects'][$objIndex]['obj'] = $obj; } return $rowData; diff --git a/src/Query/Parser.php b/src/Query/Parser.php index 38858cbf876..ac08669e642 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -6,6 +6,8 @@ use Doctrine\Common\Lexer\Token; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Exception\MissingAliasException; +use Doctrine\ORM\Exception\RepeatedAliasException; use Doctrine\ORM\Internal\Hydration\HydrationException; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; @@ -15,6 +17,7 @@ use ReflectionClass; use function array_intersect; +use function array_key_exists; use function array_search; use function assert; use function class_exists; @@ -1734,20 +1737,26 @@ public function PartialObjectExpression(): AST\PartialObjectExpression */ public function NewObjectExpression(): AST\NewObjectExpression { - $args = []; + $useNamedArguments = false; + $args = []; + $argFieldAlias = []; $this->match(TokenType::T_NEW); + if ($this->lexer->isNextToken(TokenType::T_NAMED)) { + $this->match(TokenType::T_NAMED); + $useNamedArguments = true; + } + $className = $this->AbstractSchemaName(); // note that this is not yet validated $token = $this->lexer->token; $this->match(TokenType::T_OPEN_PARENTHESIS); - $args[] = $this->NewObjectArg(); + $this->addArgument($args, $useNamedArguments); while ($this->lexer->isNextToken(TokenType::T_COMMA)) { $this->match(TokenType::T_COMMA); - - $args[] = $this->NewObjectArg(); + $this->addArgument($args, $useNamedArguments); } $this->match(TokenType::T_CLOSE_PARENTHESIS); @@ -1764,29 +1773,69 @@ public function NewObjectExpression(): AST\NewObjectExpression return $expression; } + /** @param array $args */ + public function addArgument(array &$args, bool $useNamedArguments): void + { + $fieldAlias = null; + + if ($useNamedArguments) { + $newArg = $this->NewObjectArg($fieldAlias); + + $key = $fieldAlias ?? $newArg->field; + + if ($key === null) { + throw MissingAliasException::create(); + } + + if (array_key_exists($key, $args)) { + throw RepeatedAliasException::create($key); + } + + $args[$key] = $newArg; + } else { + $args[] = $this->NewObjectArg($fieldAlias); + } + } + /** * NewObjectArg ::= ScalarExpression | "(" Subselect ")" */ - public function NewObjectArg(): mixed + public function NewObjectArg(string|null &$fieldAlias = null): mixed { + $namedArg = false; + $fieldAlias = null; + assert($this->lexer->lookahead !== null); $token = $this->lexer->lookahead; $peek = $this->lexer->glimpse(); assert($peek !== null); + + $expression = null; + + if ($token->type === TokenType::T_IDENTIFIER && $peek->value === ':') { + $fieldAlias = $this->AliasIdentificationVariable(); + $this->lexer->moveNext(); + $namedArg = true; + $token = $this->lexer->lookahead; + } + if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) { $this->match(TokenType::T_OPEN_PARENTHESIS); $expression = $this->Subselect(); $this->match(TokenType::T_CLOSE_PARENTHESIS); - - return $expression; + } elseif ($token->type === TokenType::T_NEW) { + $expression = $this->NewObjectExpression(); + } else { + $expression = $this->ScalarExpression(); } - if ($token->type === TokenType::T_NEW) { - return $this->NewObjectExpression(); + if (! $namedArg && $this->lexer->isNextToken(TokenType::T_AS)) { + $this->match(TokenType::T_AS); + $fieldAlias = $this->AliasIdentificationVariable(); } - return $this->ScalarExpression(); + return $expression; } /** diff --git a/src/Query/ResultSetMapping.php b/src/Query/ResultSetMapping.php index c95b089a73b..38ed1bf6416 100644 --- a/src/Query/ResultSetMapping.php +++ b/src/Query/ResultSetMapping.php @@ -4,7 +4,6 @@ namespace Doctrine\ORM\Query; -use function array_merge; use function count; /** @@ -552,25 +551,4 @@ public function addMetaResult( return $this; } - - public function addNewObjectAsArgument(string|int $alias, string|int $objOwner, int $objOwnerIdx): static - { - $owner = [ - 'ownerIndex' => $objOwner, - 'argIndex' => $objOwnerIdx, - ]; - - if (! isset($this->nestedNewObjectArguments[$owner['ownerIndex']])) { - $this->nestedNewObjectArguments[$alias] = $owner; - - return $this; - } - - $this->nestedNewObjectArguments = array_merge( - [$alias => $owner], - $this->nestedNewObjectArguments, - ); - - return $this; - } } diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index 46296e719e7..04d7d953ab4 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1510,6 +1510,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri $this->newObjectStack[] = [$objIndex, $argIndex]; $sqlSelectExpressions[] = $e->dispatch($this); array_pop($this->newObjectStack); + $this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex]; break; case $e instanceof AST\Subselect: @@ -1563,10 +1564,6 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri 'objIndex' => $objIndex, 'argIndex' => $argIndex, ]; - - if ($objOwner !== null && $objOwnerIdx !== null) { - $this->rsm->addNewObjectAsArgument($objIndex, $objOwner, $objOwnerIdx); - } } return implode(', ', $sqlSelectExpressions); diff --git a/src/Query/TokenType.php b/src/Query/TokenType.php index bf1c351c2a6..47cc7912711 100644 --- a/src/Query/TokenType.php +++ b/src/Query/TokenType.php @@ -89,4 +89,5 @@ enum TokenType: int case T_WHEN = 254; case T_WHERE = 255; case T_WITH = 256; + case T_NAMED = 257; } diff --git a/tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php b/tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php new file mode 100644 index 00000000000..547c7fb0c31 --- /dev/null +++ b/tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php @@ -0,0 +1,16 @@ +name = $args['name'] ?? null; + $this->email = $args['email'] ?? null; + $this->phonenumbers = $args['phonenumbers'] ?? null; + $this->address = $args['address'] ?? null; + } +} diff --git a/tests/Tests/ORM/Functional/NewOperatorTest.php b/tests/Tests/ORM/Functional/NewOperatorTest.php index 4497af517bf..64180f5dacd 100644 --- a/tests/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Tests/ORM/Functional/NewOperatorTest.php @@ -4,19 +4,25 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\ORM\Exception\MissingAliasException; +use Doctrine\ORM\Exception\RepeatedAliasException; use Doctrine\ORM\Query; use Doctrine\ORM\Query\QueryException; use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsAddressDTO; +use Doctrine\Tests\Models\CMS\CmsAddressDTONamedArgs; use Doctrine\Tests\Models\CMS\CmsEmail; use Doctrine\Tests\Models\CMS\CmsPhonenumber; use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\Models\CMS\CmsUserDTO; +use Doctrine\Tests\Models\CMS\CmsUserDTONamedArgs; +use Doctrine\Tests\Models\CMS\CmsUserDTOVariadicArg; use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use function count; +use function sprintf; #[Group('DDC-1574')] class NewOperatorTest extends OrmFunctionalTestCase @@ -1080,6 +1086,390 @@ public function testShouldSupportNestedNewOperators(): void self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']); self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']); } + + public function testNamedArguments(): void + { + $dql = <<<'SQL' + SELECT + new named CmsUserDTONamedArgs( + e.email, + u.name, + CONCAT(a.country, ' ', a.city, ' ', a.zip) AS address + ) 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 + SQL; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::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::assertSame(sprintf( + '%s %s %s', + $this->fixtures[0]->address->country, + $this->fixtures[0]->address->city, + $this->fixtures[0]->address->zip, + ), $result[0]['user']->address); + self::assertSame( + sprintf( + '%s %s %s', + $this->fixtures[1]->address->country, + $this->fixtures[1]->address->city, + $this->fixtures[1]->address->zip, + ), + $result[1]['user']->address, + ); + self::assertSame( + sprintf( + '%s %s %s', + $this->fixtures[2]->address->country, + $this->fixtures[2]->address->city, + $this->fixtures[2]->address->zip, + ), + $result[2]['user']->address, + ); + + 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 testVariadicArgument(): void + { + $dql = <<<'SQL' + SELECT + new named CmsUserDTOVariadicArg( + CONCAT(a.country, ' ', a.city, ' ', a.zip) AS address, + e.email, + u.name + ) 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 + SQL; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTOVariadicArg::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::assertSame( + sprintf( + '%s %s %s', + $this->fixtures[0]->address->country, + $this->fixtures[0]->address->city, + $this->fixtures[0]->address->zip, + ), + $result[0]['user']->address, + ); + self::assertSame( + sprintf( + '%s %s %s', + $this->fixtures[1]->address->country, + $this->fixtures[1]->address->city, + $this->fixtures[1]->address->zip, + ), + $result[1]['user']->address, + ); + self::assertSame( + sprintf( + '%s %s %s', + $this->fixtures[2]->address->country, + $this->fixtures[2]->address->city, + $this->fixtures[2]->address->zip, + ), + $result[2]['user']->address, + ); + + 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 testShouldSupportNestedNewOperatorsAndNamedArguments(): void + { + $dql = ' + SELECT + new named CmsUserDTONamedArgs( + e.email, + u.name as name, + new CmsAddressDTO( + a.country, + a.city, + a.zip + ) as addressDto + ) 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(CmsUserDTONamedArgs::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']); + + self::assertNull($result[0]['user']->address); + self::assertNull($result[1]['user']->address); + self::assertNull($result[2]['user']->address); + + self::assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->addressDto); + self::assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->addressDto); + self::assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->addressDto); + + 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::assertSame($this->fixtures[0]->address->city, $result[0]['user']->addressDto->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->addressDto->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->addressDto->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->addressDto->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->addressDto->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->addressDto->country); + + 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 testShouldSupportNestedNewOperatorsAndNamedArgumentsWithPhpNotation(): void + { + $dql = ' + SELECT + new named CmsUserDTONamedArgs( + email: e.email, + addressDto: new CmsAddressDTO( + a.country, + a.city, + a.zip + ), + name: u.name + ) 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(CmsUserDTONamedArgs::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']); + + self::assertNull($result[0]['user']->address); + self::assertNull($result[1]['user']->address); + self::assertNull($result[2]['user']->address); + + self::assertInstanceOf(CmsAddressDTO::class, $result[0]['user']->addressDto); + self::assertInstanceOf(CmsAddressDTO::class, $result[1]['user']->addressDto); + self::assertInstanceOf(CmsAddressDTO::class, $result[2]['user']->addressDto); + + 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::assertSame($this->fixtures[0]->address->city, $result[0]['user']->addressDto->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->addressDto->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->addressDto->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->addressDto->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->addressDto->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->addressDto->country); + + 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 testShouldSupportNestedNamedArguments(): void + { + $dql = ' + SELECT + new named CmsUserDTONamedArgs( + e.email, + u.name as name, + new named CmsAddressDTONamedArgs( + a.zip, + a.city, + a.country + ) as addressDtoNamedArgs + ) 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(CmsUserDTONamedArgs::class, $result[0]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']); + + self::assertNull($result[0]['user']->address); + self::assertNull($result[1]['user']->address); + self::assertNull($result[2]['user']->address); + + self::assertNull($result[0]['user']->addressDto); + self::assertNull($result[1]['user']->addressDto); + self::assertNull($result[2]['user']->addressDto); + + self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[0]['user']->addressDtoNamedArgs); + self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[1]['user']->addressDtoNamedArgs); + self::assertInstanceOf(CmsAddressDTONamedArgs::class, $result[2]['user']->addressDtoNamedArgs); + + 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::assertSame($this->fixtures[0]->address->city, $result[0]['user']->addressDtoNamedArgs->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]['user']->addressDtoNamedArgs->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]['user']->addressDtoNamedArgs->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]['user']->addressDtoNamedArgs->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]['user']->addressDtoNamedArgs->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]['user']->addressDtoNamedArgs->country); + + 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 testExceptionIfTwoAliases(): void + { + $dql = ' + SELECT + new named Doctrine\Tests\Models\CMS\CmsUserDTO( + name: u.name, + name: u.username + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u'; + + $this->expectException(RepeatedAliasException::class); + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + } + + public function testExceptionIfFunctionHasNoAlias(): void + { + $dql = " + SELECT + new named Doctrine\Tests\Models\CMS\CmsUserDTO( + u.name, + CASE WHEN (e.email = 'email@test1.com') THEN 'TEST1' ELSE 'OTHER_TEST' END + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + JOIN + u.email e"; + + $this->expectException(MissingAliasException::class); + + $query = $this->_em->createQuery($dql); + $result = $query->getResult(); + } } class ClassWithTooMuchArgs diff --git a/tests/Tests/ORM/Hydration/ResultSetMappingTest.php b/tests/Tests/ORM/Hydration/ResultSetMappingTest.php index 14b9205abfe..0c20eab0866 100644 --- a/tests/Tests/ORM/Hydration/ResultSetMappingTest.php +++ b/tests/Tests/ORM/Hydration/ResultSetMappingTest.php @@ -102,21 +102,4 @@ public function testIndexByMetadataColumn(): void self::assertTrue($this->_rsm->hasIndexBy('lu')); } - - public function testNewObjectNestedArgumentsDeepestLeavesShouldComeFirst(): void - { - $this->_rsm->addNewObjectAsArgument('objALevel2', 'objALevel1', 0); - $this->_rsm->addNewObjectAsArgument('objALevel3', 'objALevel2', 1); - $this->_rsm->addNewObjectAsArgument('objBLevel3', 'objBLevel2', 0); - $this->_rsm->addNewObjectAsArgument('objBLevel2', 'objBLevel1', 1); - - $expectedArgumentMapping = [ - 'objALevel3' => ['ownerIndex' => 'objALevel2', 'argIndex' => 1], - 'objALevel2' => ['ownerIndex' => 'objALevel1', 'argIndex' => 0], - 'objBLevel3' => ['ownerIndex' => 'objBLevel2', 'argIndex' => 0], - 'objBLevel2' => ['ownerIndex' => 'objBLevel1', 'argIndex' => 1], - ]; - - self::assertSame($expectedArgumentMapping, $this->_rsm->nestedNewObjectArguments); - } }