Skip to content

Commit 35d8636

Browse files
committed
Allow named Arguments to be passed to Dto
Allow to change argument order or use variadic argument in dto constructor.
1 parent 1153b94 commit 35d8636

File tree

9 files changed

+201
-15
lines changed

9 files changed

+201
-15
lines changed

src/Internal/Hydration/AbstractHydrator.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ abstract protected function hydrateAllData(): mixed;
253253
* data: array<array-key, array>,
254254
* newObjects?: array<array-key, array{
255255
* class: mixed,
256-
* args?: array
256+
* args?: array,
257+
* argNames?: array,
257258
* }>,
258259
* scalars?: array
259260
* }
@@ -281,8 +282,9 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
281282
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
282283
}
283284

284-
$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
285-
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
285+
$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
286+
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
287+
$rowData['newObjects'][$objIndex]['argNames'][$argIndex] = $key;
286288
break;
287289

288290
case isset($cacheKeyInfo['isScalar']):

src/Internal/Hydration/ObjectHydrator.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Doctrine\ORM\PersistentCollection;
1111
use Doctrine\ORM\Query;
1212
use Doctrine\ORM\UnitOfWork;
13+
use Doctrine\ORM\WithNamedArguments;
1314

1415
use function array_fill_keys;
1516
use function array_keys;
@@ -556,9 +557,20 @@ protected function hydrateRowData(array $row, array &$result): void
556557
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
557558

558559
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
559-
$class = $newObject['class'];
560-
$args = $newObject['args'];
561-
$obj = $class->newInstanceArgs($args);
560+
$class = $newObject['class'];
561+
$args = $newObject['args'];
562+
$argNames = $newObject['argNames'];
563+
564+
if ($class->implementsInterface(WithNamedArguments::class)) {
565+
$newArgs = [];
566+
foreach ($args as $key => $val) {
567+
$newArgs[$this->resultSetMapping()->newObjectMappings[$argNames[$key]]['argName']] = $val;
568+
}
569+
570+
$args = $newArgs;
571+
}
572+
573+
$obj = $class->newInstanceArgs($args);
562574

563575
if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
564576
$result[$resultKey] = $obj;

src/Query/AST/NewObjectExpression.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
*/
1414
class NewObjectExpression extends Node
1515
{
16-
/** @param mixed[] $args */
17-
public function __construct(public string $className, public array $args)
16+
/**
17+
* @param array<mixed> $args
18+
* @param array<?string> $argNames args key for named Arguments DTO
19+
**/
20+
public function __construct(public string $className, public array $args, public array $argNames)
1821
{
1922
}
2023

src/Query/Parser.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,25 +1626,30 @@ public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration
16261626
*/
16271627
public function NewObjectExpression(): AST\NewObjectExpression
16281628
{
1629-
$args = [];
1629+
$args = [];
1630+
$argNames = [];
1631+
16301632
$this->match(TokenType::T_NEW);
16311633

16321634
$className = $this->AbstractSchemaName(); // note that this is not yet validated
16331635
$token = $this->lexer->token;
16341636

16351637
$this->match(TokenType::T_OPEN_PARENTHESIS);
16361638

1637-
$args[] = $this->NewObjectArg();
1639+
$argName = null;
1640+
$args[] = $this->NewObjectArg($argName);
1641+
$argNames[] = $argName;
16381642

16391643
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
16401644
$this->match(TokenType::T_COMMA);
16411645

1642-
$args[] = $this->NewObjectArg();
1646+
$args[] = $this->NewObjectArg($argName);
1647+
$argNames[] = $argName;
16431648
}
16441649

16451650
$this->match(TokenType::T_CLOSE_PARENTHESIS);
16461651

1647-
$expression = new AST\NewObjectExpression($className, $args);
1652+
$expression = new AST\NewObjectExpression($className, $args, $argNames);
16481653

16491654
// Defer NewObjectExpression validation
16501655
$this->deferredNewObjectExpressions[] = [
@@ -1659,22 +1664,32 @@ public function NewObjectExpression(): AST\NewObjectExpression
16591664
/**
16601665
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
16611666
*/
1662-
public function NewObjectArg(): mixed
1667+
public function NewObjectArg(string|null &$argName = null): mixed
16631668
{
1669+
$argName = null;
16641670
assert($this->lexer->lookahead !== null);
16651671
$token = $this->lexer->lookahead;
16661672
$peek = $this->lexer->glimpse();
16671673

16681674
assert($peek !== null);
1675+
1676+
$expression = null;
1677+
16691678
if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
16701679
$this->match(TokenType::T_OPEN_PARENTHESIS);
16711680
$expression = $this->Subselect();
16721681
$this->match(TokenType::T_CLOSE_PARENTHESIS);
1682+
} else {
1683+
$expression = $this->ScalarExpression();
1684+
}
16731685

1674-
return $expression;
1686+
if ($this->lexer->isNextToken(TokenType::T_AS)) {
1687+
$this->match(TokenType::T_AS);
1688+
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
1689+
$argName = $aliasIdentificationVariable;
16751690
}
16761691

1677-
return $this->ScalarExpression();
1692+
return $expression;
16781693
}
16791694

16801695
/**

src/Query/SqlWalker.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,6 +1467,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
14671467
$resultAlias = $this->scalarResultCounter++;
14681468
$columnAlias = $this->getSQLColumnAlias('sclr');
14691469
$fieldType = 'string';
1470+
$argName = $newObjectExpression->argNames[$argIndex];
14701471

14711472
switch (true) {
14721473
case $e instanceof AST\NewObjectExpression:
@@ -1485,6 +1486,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
14851486
$fieldMapping = $class->fieldMappings[$fieldName];
14861487
$fieldType = $fieldMapping->type;
14871488
$col = trim($e->dispatch($this));
1489+
$argName ??= $newObjectExpression->args[$argIndex]->field;
14881490

14891491
$type = Type::getType($fieldType);
14901492
$col = $type->convertToPHPValueSQL($col, $this->platform);
@@ -1523,6 +1525,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
15231525
'className' => $newObjectExpression->className,
15241526
'objIndex' => $objIndex,
15251527
'argIndex' => $argIndex,
1528+
'argName' => $argName,
15261529
];
15271530
}
15281531

src/WithNamedArguments.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM;
6+
7+
/**
8+
* Interface for allow passing named arguments to Dto with NEW operator.
9+
*/
10+
interface WithNamedArguments
11+
{
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\CMS;
6+
7+
use Doctrine\ORM\WithNamedArguments;
8+
9+
class CmsUserDTONamedArgs implements WithNamedArguments
10+
{
11+
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $address = null, public int|null $phonenumbers = null)
12+
{
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\CMS;
6+
7+
use Doctrine\ORM\WithNamedArguments;
8+
9+
class CmsUserDTOVariadicArg implements WithNamedArguments
10+
{
11+
public string|null $name = null;
12+
public string|null $email = null;
13+
public string|null $address = null;
14+
public int|null $phonenumbers = null;
15+
16+
public function __construct(...$args)
17+
{
18+
$this->name = $args['name'] ?? null;
19+
$this->email = $args['email'] ?? null;
20+
$this->phonenumbers = $args['phonenumbers'] ?? null;
21+
$this->address = $args['address'] ?? null;
22+
}
23+
}

tests/Tests/ORM/Functional/NewOperatorTest.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
1313
use Doctrine\Tests\Models\CMS\CmsUser;
1414
use Doctrine\Tests\Models\CMS\CmsUserDTO;
15+
use Doctrine\Tests\Models\CMS\CmsUserDTONamedArgs;
16+
use Doctrine\Tests\Models\CMS\CmsUserDTOVariadicArg;
1517
use Doctrine\Tests\OrmFunctionalTestCase;
1618
use PHPUnit\Framework\Attributes\DataProvider;
1719
use PHPUnit\Framework\Attributes\Group;
@@ -1013,6 +1015,106 @@ public function testClassCantBeInstantiatedException(): void
10131015
$dql = 'SELECT new Doctrine\Tests\ORM\Functional\ClassWithPrivateConstructor(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u';
10141016
$this->_em->createQuery($dql)->getResult();
10151017
}
1018+
1019+
public function testNamedArguments(): void
1020+
{
1021+
$dql = '
1022+
SELECT
1023+
new CmsUserDTONamedArgs(
1024+
e.email,
1025+
u.name,
1026+
CONCAT(a.country, \' \', a.city, \' \', a.zip) AS address
1027+
) as user,
1028+
u.status,
1029+
u.username as cmsUserUsername
1030+
FROM
1031+
Doctrine\Tests\Models\CMS\CmsUser u
1032+
JOIN
1033+
u.email e
1034+
JOIN
1035+
u.address a
1036+
ORDER BY
1037+
u.name';
1038+
1039+
$query = $this->getEntityManager()->createQuery($dql);
1040+
$result = $query->getResult();
1041+
1042+
self::assertCount(3, $result);
1043+
1044+
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']);
1045+
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']);
1046+
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']);
1047+
1048+
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
1049+
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
1050+
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
1051+
1052+
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
1053+
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
1054+
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
1055+
1056+
self::assertSame($this->fixtures[0]->address->country . ' ' . $this->fixtures[0]->address->city . ' ' . $this->fixtures[0]->address->zip, $result[0]['user']->address);
1057+
self::assertSame($this->fixtures[1]->address->country . ' ' . $this->fixtures[1]->address->city . ' ' . $this->fixtures[1]->address->zip, $result[1]['user']->address);
1058+
self::assertSame($this->fixtures[2]->address->country . ' ' . $this->fixtures[2]->address->city . ' ' . $this->fixtures[2]->address->zip, $result[2]['user']->address);
1059+
1060+
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
1061+
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
1062+
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
1063+
1064+
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
1065+
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
1066+
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
1067+
}
1068+
1069+
public function testVariadicArgument(): void
1070+
{
1071+
$dql = '
1072+
SELECT
1073+
new CmsUserDTOVariadicArg(
1074+
CONCAT(a.country, \' \', a.city, \' \', a.zip) AS address,
1075+
e.email,
1076+
u.name
1077+
) as user,
1078+
u.status,
1079+
u.username as cmsUserUsername
1080+
FROM
1081+
Doctrine\Tests\Models\CMS\CmsUser u
1082+
JOIN
1083+
u.email e
1084+
JOIN
1085+
u.address a
1086+
ORDER BY
1087+
u.name';
1088+
1089+
$query = $this->getEntityManager()->createQuery($dql);
1090+
$result = $query->getResult();
1091+
1092+
self::assertCount(3, $result);
1093+
1094+
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]['user']);
1095+
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]['user']);
1096+
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]['user']);
1097+
1098+
self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
1099+
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
1100+
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);
1101+
1102+
self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
1103+
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
1104+
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);
1105+
1106+
self::assertSame($this->fixtures[0]->address->country . ' ' . $this->fixtures[0]->address->city . ' ' . $this->fixtures[0]->address->zip, $result[0]['user']->address);
1107+
self::assertSame($this->fixtures[1]->address->country . ' ' . $this->fixtures[1]->address->city . ' ' . $this->fixtures[1]->address->zip, $result[1]['user']->address);
1108+
self::assertSame($this->fixtures[2]->address->country . ' ' . $this->fixtures[2]->address->city . ' ' . $this->fixtures[2]->address->zip, $result[2]['user']->address);
1109+
1110+
self::assertSame($this->fixtures[0]->status, $result[0]['status']);
1111+
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
1112+
self::assertSame($this->fixtures[2]->status, $result[2]['status']);
1113+
1114+
self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
1115+
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
1116+
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
1117+
}
10161118
}
10171119

10181120
class ClassWithTooMuchArgs

0 commit comments

Comments
 (0)