Skip to content

Commit

Permalink
add capability to hydrate an entity in a dto
Browse files Browse the repository at this point in the history
this PR allow to hydrate data in an entity  nested in a dto
  • Loading branch information
eltharin committed Feb 21, 2025
1 parent 8873109 commit 958aaec
Show file tree
Hide file tree
Showing 8 changed files with 513 additions and 65 deletions.
23 changes: 17 additions & 6 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,16 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.

You can hydrate entities in a Dto :

.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, a AS address) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}
Using INDEX BY
~~~~~~~~~~~~~~

Expand Down Expand Up @@ -1697,12 +1707,13 @@ Select Expressions

.. code-block:: php
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]
EntityAsDtoArgumentExpression ::= IdentificationVariable
Conditional Expressions
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
30 changes: 27 additions & 3 deletions src/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use LogicException;
use ReflectionClass;

use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function count;
Expand Down Expand Up @@ -336,9 +338,21 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
}
}

foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
if (! isset($rowData['newObjects'][$ownerIndex . ':' . $argIndex])) {
continue;
$nestedEntities = [];
foreach ($this->resultSetMapping()->nestedNewObjectArguments as ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex, 'argAlias' => $argAlias]) {
if (array_key_exists($argAlias, $rowData['newObjects'])) {
ksort($rowData['newObjects'][$argAlias]['args']);

Check failure on line 344 in src/Internal/Hydration/AbstractHydrator.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (PHP: 8.4)

Function ksort() should not be referenced via a fallback global name, but via a use statement.
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['newObjects'][$argAlias]['class']->newInstanceArgs($rowData['newObjects'][$argAlias]['args']);
unset($rowData['newObjects'][$argAlias]);
} elseif (array_key_exists($argAlias, $rowData['data'])) {
if (! array_key_exists($argAlias, $nestedEntities)) {
$nestedEntities[$argAlias] = '';
$rowData['data'][$argAlias] = $this->hydrateNestedEnity($rowData['data'][$argAlias], $argAlias);

Check failure on line 350 in src/Internal/Hydration/AbstractHydrator.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Parameter #2 $dqlAlias of method Doctrine\ORM\Internal\Hydration\AbstractHydrator::hydrateNestedEnity() expects string, int|string given.

Check failure on line 350 in src/Internal/Hydration/AbstractHydrator.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Parameter #2 $dqlAlias of method Doctrine\ORM\Internal\Hydration\AbstractHydrator::hydrateNestedEnity() expects string, int|string given.
}

$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $rowData['data'][$argAlias];
} else {
throw new LogicException($argAlias . ' not exist');
}

$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
Expand All @@ -349,6 +363,10 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
}

foreach (array_keys($nestedEntities) as $entity) {
unset($rowData['data'][$entity]);
}

foreach ($rowData['newObjects'] as $objIndex => $newObject) {
$obj = $newObject['class']->newInstanceArgs($newObject['args']);

Expand All @@ -358,6 +376,12 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
return $rowData;
}

/** @param mixed[] $data pre-hydrated SQL Result Row. */
protected function hydrateNestedEnity(array $data, string $dqlAlias): mixed
{
return $data;
}

/**
* Processes a row of the result set.
*
Expand Down
14 changes: 14 additions & 0 deletions src/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ protected function prepare(): void
$parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];

if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
continue;
}

throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
}

Expand Down Expand Up @@ -569,6 +573,16 @@ protected function hydrateRowData(array $row, array &$result): void
}
}

/** @param mixed[] $data pre-hydrated SQL Result Row. */
protected function hydrateNestedEnity(array $data, string $dqlAlias): mixed
{
if (isset($this->resultSetMapping()->nestedEntities[$dqlAlias])) {
return $this->getEntity($data, $dqlAlias);
}

return $data;
}

/**
* When executed in a hydrate() loop we may have to clear internal state to
* decrease memory consumption.
Expand Down
26 changes: 26 additions & 0 deletions src/Query/AST/EntityAsDtoArgumentExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM\Query\AST;

use Doctrine\ORM\Query\SqlWalker;

/**
* EntityAsDtoArgumentExpression ::= IdentificationVariable
*
* @link www.doctrine-project.org
*/
class EntityAsDtoArgumentExpression extends Node
{
public function __construct(
public mixed $expression,
public string|null $identificationVariable,
) {
}

public function dispatch(SqlWalker $walker): string
{
return $walker->walkEntityAsDtoArgumentExpression($this);
}
}
46 changes: 46 additions & 0 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,50 @@ public function CollectionValuedPathExpression(): AST\PathExpression
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
}

/**
* EntityAsDtoArgumentExpression ::= IdentificationVariable
*/
public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpression
{
assert($this->lexer->lookahead !== null);
$expression = null;
$identVariable = null;
$peek = $this->lexer->glimpse();
$lookaheadType = $this->lexer->lookahead->type;
assert($peek !== null);

assert($lookaheadType === TokenType::T_IDENTIFIER);
assert($peek->type !== TokenType::T_DOT);
assert($peek->type !== TokenType::T_OPEN_PARENTHESIS);

$expression = $identVariable = $this->IdentificationVariable();

// [["AS"] AliasResultVariable]
$mustHaveAliasResultVariable = false;

if ($this->lexer->isNextToken(TokenType::T_AS)) {
$this->match(TokenType::T_AS);

$mustHaveAliasResultVariable = true;
}

$aliasResultVariable = null;

if ($mustHaveAliasResultVariable || $this->lexer->isNextToken(TokenType::T_IDENTIFIER)) {
$token = $this->lexer->lookahead;
$aliasResultVariable = $this->AliasResultVariable();

// Include AliasResultVariable in query components.
$this->queryComponents[$aliasResultVariable] = [
'resultVariable' => $expression,
'nestingLevel' => $this->nestingLevel,
'token' => $token,
];
}

return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
}

/**
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
*/
Expand Down Expand Up @@ -1848,6 +1892,8 @@ public function NewObjectArg(string|null &$fieldAlias = null): mixed
$this->match(TokenType::T_CLOSE_PARENTHESIS);
} elseif ($token->type === TokenType::T_NEW) {
$expression = $this->NewObjectExpression();
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
$expression = $this->EntityAsDtoArgumentExpression();
} else {
$expression = $this->ScalarExpression();
}
Expand Down
7 changes: 7 additions & 0 deletions src/Query/ResultSetMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ class ResultSetMapping
*/
public array $discriminatorParameters = [];

/**
* Entities nested in Dto's
*
* @phpstan-var array<string, string>
*/
public array $nestedEntities = [];

/**
* Adds an entity result to this ResultSetMapping.
*
Expand Down
Loading

0 comments on commit 958aaec

Please sign in to comment.