Skip to content

Commit f694f11

Browse files
committed
add capability to hydrate an entity in a dto
this PR allow to hydrate data in an entity nested in a dto
1 parent 742eead commit f694f11

File tree

9 files changed

+378
-68
lines changed

9 files changed

+378
-68
lines changed

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,16 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
674674
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
675675
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
676676

677+
You can hydrate entities in a Dto :
678+
679+
.. code-block:: php
680+
681+
<?php
682+
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, a AS address) FROM Customer c JOIN c.address a');
683+
$users = $query->getResult(); // array of CustomerDTO
684+
685+
// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}
686+
677687
Using INDEX BY
678688
~~~~~~~~~~~~~~
679689

@@ -1697,12 +1707,13 @@ Select Expressions
16971707

16981708
.. code-block:: php
16991709
1700-
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
1701-
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
1702-
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1703-
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1704-
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1705-
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
1710+
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
1711+
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
1712+
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
1713+
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
1714+
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1715+
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]
1716+
EntityAsDtoArgumentExpression ::= IdentificationVariable
17061717
17071718
Conditional Expressions
17081719
~~~~~~~~~~~~~~~~~~~~~~~

phpstan-baseline.neon

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2796,12 +2796,6 @@ parameters:
27962796
count: 1
27972797
path: src/Query/SqlWalker.php
27982798

2799-
-
2800-
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses \(array\<string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: string, resultAlias\: string\|null\}\>\) does not accept non\-empty\-array\<int\|string, array\{class\: Doctrine\\ORM\\Mapping\\ClassMetadata, dqlAlias\: mixed, resultAlias\: string\|null\}\>\.$#'
2801-
identifier: assign.propertyType
2802-
count: 1
2803-
path: src/Query/SqlWalker.php
2804-
28052799
-
28062800
message: '#^Property Doctrine\\ORM\\Query\\SqlWalker\:\:\$selectedClasses with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
28072801
identifier: missingType.generics

src/Internal/Hydration/AbstractHydrator.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use ReflectionClass;
2020

2121
use function array_key_exists;
22+
use function array_keys;
2223
use function array_map;
2324
use function array_merge;
2425
use function count;
@@ -348,14 +349,28 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
348349
}
349350
}
350351

352+
$nestedEntities = [];
351353
foreach ($this->resultSetMapping()->nestedNewObjectArguments as ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex, 'argAlias' => $argAlias]) {
352354
if (array_key_exists($argAlias, $rowData['newObjects'])) {
353355
ksort($rowData['newObjects'][$argAlias]['args']);
354356
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['newObjects'][$argAlias]['class']->newInstanceArgs($rowData['newObjects'][$argAlias]['args']);
355357
unset($rowData['newObjects'][$argAlias]);
358+
} elseif (array_key_exists($argAlias, $rowData['data'])) {
359+
if (! array_key_exists($argAlias, $nestedEntities)) {
360+
$nestedEntities[$argAlias] = '';
361+
$rowData['data'][$argAlias] = $this->hydrateNestedEnity($rowData['data'][$argAlias], $argAlias);
362+
}
363+
364+
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['data'][$argAlias];
365+
} else {
366+
throw new LogicException($argAlias . ' not exist');
356367
}
357368
}
358369

370+
foreach (array_keys($nestedEntities) as $entity) {
371+
unset($rowData['data'][$entity]);
372+
}
373+
359374
foreach ($rowData['newObjects'] as $objIndex => $newObject) {
360375
ksort($rowData['newObjects'][$objIndex]['args']);
361376
$obj = $rowData['newObjects'][$objIndex]['class']->newInstanceArgs($rowData['newObjects'][$objIndex]['args']);
@@ -366,6 +381,12 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
366381
return $rowData;
367382
}
368383

384+
/** @param mixed[] $data pre-hydrated SQL Result Row. */
385+
protected function hydrateNestedEnity(array $data, string $dqlAlias): mixed
386+
{
387+
return $data;
388+
}
389+
369390
/**
370391
* Processes a row of the result set.
371392
*

src/Internal/Hydration/ObjectHydrator.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ protected function prepare(): void
7070
$parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
7171

7272
if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
73+
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
74+
continue;
75+
}
76+
7377
throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
7478
}
7579

@@ -569,6 +573,16 @@ protected function hydrateRowData(array $row, array &$result): void
569573
}
570574
}
571575

576+
/** @param mixed[] $data pre-hydrated SQL Result Row. */
577+
protected function hydrateNestedEnity(array $data, string $dqlAlias): mixed
578+
{
579+
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
580+
return $this->getEntity($data, $dqlAlias);
581+
}
582+
583+
return $data;
584+
}
585+
572586
/**
573587
* When executed in a hydrate() loop we may have to clear internal state to
574588
* decrease memory consumption.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Query\AST;
6+
7+
use Doctrine\ORM\Query\SqlWalker;
8+
9+
/**
10+
* EntityAsDtoArgumentExpression ::= IdentificationVariable
11+
*
12+
* @link www.doctrine-project.org
13+
*/
14+
class EntityAsDtoArgumentExpression extends Node
15+
{
16+
public function __construct(
17+
public mixed $expression,
18+
public string|null $identificationVariable,
19+
) {
20+
}
21+
22+
public function dispatch(SqlWalker $walker): string
23+
{
24+
return $walker->walkEntityAsDtoArgumentExpression($this);
25+
}
26+
}

src/Query/Parser.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,50 @@ public function CollectionValuedPathExpression(): AST\PathExpression
11061106
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
11071107
}
11081108

1109+
/**
1110+
* EntityAsDtoArgumentExpression ::= IdentificationVariable
1111+
*/
1112+
public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpression
1113+
{
1114+
assert($this->lexer->lookahead !== null);
1115+
$expression = null;
1116+
$identVariable = null;
1117+
$peek = $this->lexer->glimpse();
1118+
$lookaheadType = $this->lexer->lookahead->type;
1119+
assert($peek !== null);
1120+
1121+
assert($lookaheadType === TokenType::T_IDENTIFIER);
1122+
assert($peek->type !== TokenType::T_DOT);
1123+
assert($peek->type !== TokenType::T_OPEN_PARENTHESIS);
1124+
1125+
$expression = $identVariable = $this->IdentificationVariable();
1126+
1127+
// [["AS"] AliasResultVariable]
1128+
$mustHaveAliasResultVariable = false;
1129+
1130+
if ($this->lexer->isNextToken(TokenType::T_AS)) {
1131+
$this->match(TokenType::T_AS);
1132+
1133+
$mustHaveAliasResultVariable = true;
1134+
}
1135+
1136+
$aliasResultVariable = null;
1137+
1138+
if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
1139+
$token = $this->lexer->lookahead;
1140+
$aliasResultVariable = $this->AliasResultVariable();
1141+
1142+
// Include AliasResultVariable in query components.
1143+
$this->queryComponents[$aliasResultVariable] = [
1144+
'resultVariable' => $expression,
1145+
'nestingLevel' => $this->nestingLevel,
1146+
'token' => $token,
1147+
];
1148+
}
1149+
1150+
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
1151+
}
1152+
11091153
/**
11101154
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
11111155
*/
@@ -1849,6 +1893,8 @@ public function NewObjectArg(string|null &$fieldAlias = null): mixed
18491893
$this->match(TokenType::T_CLOSE_PARENTHESIS);
18501894
} elseif ($token->type === TokenType::T_NEW) {
18511895
$expression = $this->NewObjectExpression();
1896+
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
1897+
$expression = $this->EntityAsDtoArgumentExpression();
18521898
} else {
18531899
$expression = $this->ScalarExpression();
18541900
}

src/Query/ResultSetMapping.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ class ResultSetMapping
187187
*/
188188
public array $discriminatorParameters = [];
189189

190+
/**
191+
* Entities nested in Dto's
192+
*
193+
* @phpstan-var array<string, array<string, (int|string)>>
194+
*/
195+
public array $nestedEntities = [];
196+
190197
/**
191198
* Adds an entity result to this ResultSetMapping.
192199
*

0 commit comments

Comments
 (0)