Skip to content

Commit 2a83338

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 6f93ceb commit 2a83338

14 files changed

+412
-77
lines changed

docs/en/reference/dql-doctrine-query-language.rst

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,38 @@ You can also nest several DTO :
608608
609609
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.
610610

611+
If you use your data-transfer objects for multiple query but not ever the same parameters, you can use named arguments or variadic arguments, add ``WithNamedArguments`` to your Dto :
612+
613+
.. code-block:: php
614+
615+
<?php
616+
617+
use Doctrine\ORM\WithNamedArguments;
618+
619+
class CustomerDTO implements WithNamedArguments
620+
{
621+
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $city = null, public mixed|null $value = null)
622+
{
623+
}
624+
}
625+
626+
And then you can select the fields you want in order you want :
627+
628+
.. code-block:: php
629+
630+
<?php
631+
$query = $em->createQuery('SELECT NEW CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a');
632+
$users = $query->getResult(); // array of CustomerDTO
633+
634+
you can either aliases column :
635+
636+
.. code-block:: php
637+
638+
<?php
639+
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a');
640+
$users = $query->getResult(); // array of CustomerDTO
641+
642+
611643
Using INDEX BY
612644
~~~~~~~~~~~~~~
613645

phpcs.xml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,9 @@
273273
<!-- https://github.com/doctrine/orm/issues/8537 -->
274274
<exclude-pattern>src/QueryBuilder.php</exclude-pattern>
275275
</rule>
276+
277+
<rule ref="Generic.Files.LineEndings.InvalidEOLChar">
278+
<exclude name="Generic.Files.LineEndings.InvalidEOLChar"/>
279+
</rule>
280+
276281
</ruleset>

psalm-baseline.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,8 @@
217217
<code><![CDATA[return $rowData;]]></code>
218218
</ReferenceConstraintViolation>
219219
<PossiblyUndefinedArrayOffset>
220-
<code><![CDATA[$newObject['args']]]></code>
221-
<code><![CDATA[$newObject['args']]]></code>
220+
<code><![CDATA[$newObject['argColumnNames']]]></code>
221+
<code><![CDATA[$newObject['argColumnNames']]]></code>
222222
</PossiblyUndefinedArrayOffset>
223223
</file>
224224
<file src="src/Internal/Hydration/ArrayHydrator.php">

src/Internal/Hydration/AbstractHydrator.php

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
use Doctrine\ORM\Query\ResultSetMapping;
1515
use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
1616
use Doctrine\ORM\UnitOfWork;
17+
use Doctrine\ORM\WithNamedArguments;
1718
use Generator;
1819
use LogicException;
1920
use ReflectionClass;
2021

22+
use function array_key_exists;
2123
use function array_map;
2224
use function array_merge;
2325
use function count;
@@ -254,14 +256,15 @@ abstract protected function hydrateAllData(): mixed;
254256
* newObjects?: array<array-key, array{
255257
* class: ReflectionClass,
256258
* args: array,
259+
* argNames?: array,
257260
* obj: object
258261
* }>,
259262
* scalars?: array
260263
* }
261264
*/
262265
protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
263266
{
264-
$rowData = ['data' => []];
267+
$rowData = ['data' => [], 'newObjects' => []];
265268

266269
foreach ($data as $key => $value) {
267270
$cacheKeyInfo = $this->hydrateColumnInfo($key);
@@ -282,12 +285,10 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
282285
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
283286
}
284287

285-
if (! isset($rowData['newObjects'])) {
286-
$rowData['newObjects'] = [];
287-
}
288+
$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
289+
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
290+
$rowData['newObjects'][$objIndex]['argColumnNames'][$argIndex] = $key;
288291

289-
$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
290-
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
291292
break;
292293

293294
case isset($cacheKeyInfo['isScalar']):
@@ -341,33 +342,54 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
341342
}
342343

343344
foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
344-
if (! isset($rowData['newObjects'][$objIndex])) {
345+
if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) {
345346
continue;
346347
}
347348

348-
$newObject = $rowData['newObjects'][$objIndex];
349-
unset($rowData['newObjects'][$objIndex]);
349+
$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
350+
unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]);
350351

351-
$class = $newObject['class'];
352-
$args = $newObject['args'];
353-
$obj = $class->newInstanceArgs($args);
352+
$class = $newObject['class'];
353+
$args = $newObject['args'];
354+
$argColumnNames = $newObject['argColumnNames'];
355+
$obj = $this->getNewObjectInstance($class, $args, $argColumnNames);
354356

355-
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
357+
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
358+
$rowData['newObjects'][$ownerIndex]['argColumnNames'][$argIndex] = $objIndex;
356359
}
357360

358-
if (isset($rowData['newObjects'])) {
359-
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
360-
$class = $newObject['class'];
361-
$args = $newObject['args'];
362-
$obj = $class->newInstanceArgs($args);
361+
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
362+
$class = $newObject['class'];
363+
$args = $newObject['args'];
364+
$argColumnNames = $newObject['argColumnNames'];
365+
$obj = $this->getNewObjectInstance($class, $args, $argColumnNames);
363366

364-
$rowData['newObjects'][$objIndex]['obj'] = $obj;
365-
}
367+
$rowData['newObjects'][$objIndex]['obj'] = $obj;
366368
}
367369

368370
return $rowData;
369371
}
370372

373+
/**
374+
* @param array<mixed> $args
375+
* @param array<string> $argColumnNames
376+
*/
377+
private function getNewObjectInstance(ReflectionClass $class, array $args, array $argColumnNames): object
378+
{
379+
if ($class->implementsInterface(WithNamedArguments::class)) {
380+
$newArgs = [];
381+
foreach ($args as $key => $val) {
382+
if (array_key_exists($key, $argColumnNames)) {
383+
$newArgs[$this->resultSetMapping()->newObjectMappings[$argColumnNames[$key]]['objAlias']] = $val;
384+
}
385+
}
386+
387+
$args = $newArgs;
388+
}
389+
390+
return $class->newInstanceArgs($args);
391+
}
392+
371393
/**
372394
* Processes a row of the result set.
373395
*

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> $argFieldAlias args key for named Arguments DTO
19+
**/
20+
public function __construct(public string $className, public array $args, public array $argFieldAlias)
1821
{
1922
}
2023

src/Query/Parser.php

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,25 +1734,30 @@ public function PartialObjectExpression(): AST\PartialObjectExpression
17341734
*/
17351735
public function NewObjectExpression(): AST\NewObjectExpression
17361736
{
1737-
$args = [];
1737+
$args = [];
1738+
$argFieldAlias = [];
17381739
$this->match(TokenType::T_NEW);
17391740

17401741
$className = $this->AbstractSchemaName(); // note that this is not yet validated
17411742
$token = $this->lexer->token;
17421743

17431744
$this->match(TokenType::T_OPEN_PARENTHESIS);
17441745

1745-
$args[] = $this->NewObjectArg();
1746+
$fieldAlias = null;
1747+
$args[] = $this->NewObjectArg($fieldAlias);
1748+
$argFieldAlias[] = $fieldAlias;
17461749

17471750
while ($this->lexer->isNextToken(TokenType::T_COMMA)) {
17481751
$this->match(TokenType::T_COMMA);
17491752

1750-
$args[] = $this->NewObjectArg();
1753+
$fieldAlias = null;
1754+
$args[] = $this->NewObjectArg($fieldAlias);
1755+
$argFieldAlias[] = $fieldAlias;
17511756
}
17521757

17531758
$this->match(TokenType::T_CLOSE_PARENTHESIS);
17541759

1755-
$expression = new AST\NewObjectExpression($className, $args);
1760+
$expression = new AST\NewObjectExpression($className, $args, $argFieldAlias);
17561761

17571762
// Defer NewObjectExpression validation
17581763
$this->deferredNewObjectExpressions[] = [
@@ -1767,26 +1772,34 @@ public function NewObjectExpression(): AST\NewObjectExpression
17671772
/**
17681773
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
17691774
*/
1770-
public function NewObjectArg(): mixed
1775+
public function NewObjectArg(string|null &$fieldAlias = null): mixed
17711776
{
1777+
$fieldAlias = null;
1778+
17721779
assert($this->lexer->lookahead !== null);
17731780
$token = $this->lexer->lookahead;
17741781
$peek = $this->lexer->glimpse();
17751782

17761783
assert($peek !== null);
1784+
1785+
$expression = null;
1786+
17771787
if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
17781788
$this->match(TokenType::T_OPEN_PARENTHESIS);
17791789
$expression = $this->Subselect();
17801790
$this->match(TokenType::T_CLOSE_PARENTHESIS);
1781-
1782-
return $expression;
1791+
} elseif ($token->type === TokenType::T_NEW) {
1792+
$expression = $this->NewObjectExpression();
1793+
} else {
1794+
$expression = $this->ScalarExpression();
17831795
}
17841796

1785-
if ($token->type === TokenType::T_NEW) {
1786-
return $this->NewObjectExpression();
1797+
if ($this->lexer->isNextToken(TokenType::T_AS)) {
1798+
$this->match(TokenType::T_AS);
1799+
$fieldAlias = $this->AliasIdentificationVariable();
17871800
}
17881801

1789-
return $this->ScalarExpression();
1802+
return $expression;
17901803
}
17911804

17921805
/**

src/Query/ResultSetMapping.php

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Doctrine\ORM\Query;
66

7-
use function array_merge;
87
use function count;
98

109
/**
@@ -552,25 +551,4 @@ public function addMetaResult(
552551

553552
return $this;
554553
}
555-
556-
public function addNewObjectAsArgument(string|int $alias, string|int $objOwner, int $objOwnerIdx): static
557-
{
558-
$owner = [
559-
'ownerIndex' => $objOwner,
560-
'argIndex' => $objOwnerIdx,
561-
];
562-
563-
if (! isset($this->nestedNewObjectArguments[$owner['ownerIndex']])) {
564-
$this->nestedNewObjectArguments[$alias] = $owner;
565-
566-
return $this;
567-
}
568-
569-
$this->nestedNewObjectArguments = array_merge(
570-
[$alias => $owner],
571-
$this->nestedNewObjectArguments,
572-
);
573-
574-
return $this;
575-
}
576554
}

src/Query/SqlWalker.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,12 +1504,14 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
15041504
$resultAlias = $this->scalarResultCounter++;
15051505
$columnAlias = $this->getSQLColumnAlias('sclr');
15061506
$fieldType = 'string';
1507+
$objAlias = $newObjectExpression->argFieldAlias[$argIndex];
15071508

15081509
switch (true) {
15091510
case $e instanceof AST\NewObjectExpression:
15101511
$this->newObjectStack[] = [$objIndex, $argIndex];
15111512
$sqlSelectExpressions[] = $e->dispatch($this);
15121513
array_pop($this->newObjectStack);
1514+
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex];
15131515
break;
15141516

15151517
case $e instanceof AST\Subselect:
@@ -1524,6 +1526,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
15241526
$fieldMapping = $class->fieldMappings[$fieldName];
15251527
$fieldType = $fieldMapping->type;
15261528
$col = trim($e->dispatch($this));
1529+
$objAlias ??= $newObjectExpression->args[$argIndex]->field;
15271530

15281531
$type = Type::getType($fieldType);
15291532
$col = $type->convertToPHPValueSQL($col, $this->platform);
@@ -1562,11 +1565,8 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
15621565
'className' => $newObjectExpression->className,
15631566
'objIndex' => $objIndex,
15641567
'argIndex' => $argIndex,
1568+
'objAlias' => $objAlias,
15651569
];
1566-
1567-
if ($objOwner !== null && $objOwnerIdx !== null) {
1568-
$this->rsm->addNewObjectAsArgument($objIndex, $objOwner, $objOwnerIdx);
1569-
}
15701570
}
15711571

15721572
return implode(', ', $sqlSelectExpressions);

src/WithNamedArguments.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM;
6+
7+
interface WithNamedArguments
8+
{
9+
}
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 CmsAddressDTONamedArgs implements WithNamedArguments
10+
{
11+
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null, public CmsAddressDTO|string|null $address = null)
12+
{
13+
}
14+
}

0 commit comments

Comments
 (0)