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);
- }
}