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 708bd84 commit 662a6cc
Show file tree
Hide file tree
Showing 8 changed files with 368 additions and 62 deletions.
13 changes: 7 additions & 6 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1697,12 +1697,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
21 changes: 21 additions & 0 deletions src/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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 @@ -348,14 +349,28 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
}
}

$nestedEntities = [];
foreach ($this->resultSetMapping()->nestedNewObjectArguments as ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex, 'argAlias' => $argAlias]) {
if (array_key_exists($argAlias, $rowData['newObjects'])) {
ksort($rowData['newObjects'][$argAlias]['args']);
$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 361 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 361 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');
}
}

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

foreach ($rowData['newObjects'] as $objIndex => $newObject) {
ksort($rowData['newObjects'][$objIndex]['args']);
$obj = $rowData['newObjects'][$objIndex]['class']->newInstanceArgs($rowData['newObjects'][$objIndex]['args']);
Expand All @@ -366,6 +381,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 @@ -1849,6 +1893,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 @@ -187,6 +187,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
139 changes: 83 additions & 56 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,14 @@ public function walkEntityIdentificationVariable(string $identVariable): string
return implode(', ', $sqlParts);
}

/**
* Walks down an EntityAsDtoArgumentExpression AST node, thereby generating the appropriate SQL.
*/
public function walkEntityAsDtoArgumentExpression(AST\EntityAsDtoArgumentExpression $expr): string
{
return implode(', ', $this->walkObjectExpression($expr->expression, [], $expr->identificationVariable ?: null));
}

/**
* Walks down an IdentificationVariable (no AST node associated), thereby generating the SQL.
*/
Expand Down Expand Up @@ -1356,84 +1364,95 @@ public function walkSelectExpression(AST\SelectExpression $selectExpression): st
$partialFieldSet = [];
}

$class = $this->getMetadataForDqlAlias($dqlAlias);
$resultAlias = $selectExpression->fieldIdentificationVariable ?: null;
$sql .= implode(', ', $this->walkObjectExpression($dqlAlias, $partialFieldSet, $selectExpression->fieldIdentificationVariable ?: null));
}

if (! isset($this->selectedClasses[$dqlAlias])) {
$this->selectedClasses[$dqlAlias] = [
'class' => $class,
'dqlAlias' => $dqlAlias,
'resultAlias' => $resultAlias,
];
}
return $sql;
}

$sqlParts = [];
/**
* Walks down an Object Expression AST node and return Sql Parts
*
* @param mixed[] $partialFieldSet
*
* @return string[]
*/
public function walkObjectExpression(string $dqlAlias, array $partialFieldSet, string|null $resultAlias): array
{
$class = $this->getMetadataForDqlAlias($dqlAlias);

// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
continue;
}
if (! isset($this->selectedClasses[$dqlAlias])) {
$this->selectedClasses[$dqlAlias] = [
'class' => $class,
'dqlAlias' => $dqlAlias,
'resultAlias' => $resultAlias,
];
}

$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
: $class->getTableName();
$sqlParts = [];

$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
if ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true)) {
continue;
}

$col = $sqlTableAlias . '.' . $quotedColumnName;
$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
: $class->getTableName();

$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);

$sqlParts[] = $col . ' AS ' . $columnAlias;
$col = $sqlTableAlias . '.' . $quotedColumnName;

$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);

$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
$sqlParts[] = $col . ' AS ' . $columnAlias;

if (! empty($mapping->enumType)) {
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
}
}
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;

// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance only if partial objects are disallowed,
// since it requires outer joining subtables.
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);

foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}
if (! empty($mapping->enumType)) {
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);
}
}

$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);
// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance only if partial objects are disallowed,
// since it requires outer joining subtables.
if ($class->isInheritanceTypeSingleTable() || ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD)) {
foreach ($class->subClasses as $subClassName) {
$subClass = $this->em->getClassMetadata($subClassName);
$sqlTableAlias = $this->getSQLTableAlias($subClass->getTableName(), $dqlAlias);

foreach ($subClass->fieldMappings as $fieldName => $mapping) {
if (isset($mapping->inherited) || ($partialFieldSet && ! in_array($fieldName, $partialFieldSet, true))) {
continue;
}

$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $subClass, $this->platform);

$col = $sqlTableAlias . '.' . $quotedColumnName;
$col = $sqlTableAlias . '.' . $quotedColumnName;

$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);
$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);

$sqlParts[] = $col . ' AS ' . $columnAlias;
$sqlParts[] = $col . ' AS ' . $columnAlias;

$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;
$this->scalarResultAliasMap[$resultAlias][] = $columnAlias;

$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}
}
$this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName);
}

$sql .= implode(', ', $sqlParts);
}
}

return $sql;
return $sqlParts;
}

public function walkQuantifiedExpression(AST\QuantifiedExpression $qExpr): string
Expand Down Expand Up @@ -1549,6 +1568,14 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;

case $e instanceof AST\EntityAsDtoArgumentExpression:
$alias = $e->identificationVariable ?: $columnAlias;
$this->rsm->nestedNewObjectArguments[$columnAlias] = ['ownerIndex' => $objIndex, 'argIndex' => $argIndex, 'argAlias' => $alias];
$this->rsm->nestedEntities[$alias] = ['parent' => $objIndex, 'argIndex' => $argIndex, 'type' => 'entity'];

Check failure on line 1574 in src/Query/SqlWalker.php

View workflow job for this annotation

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

Property Doctrine\ORM\Query\ResultSetMapping::$nestedEntities (array<string, string>) does not accept array<string, array<string, (int|string)>|string>.

Check failure on line 1574 in src/Query/SqlWalker.php

View workflow job for this annotation

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

Property Doctrine\ORM\Query\ResultSetMapping::$nestedEntities (array<string, string>) does not accept array<string, array<string, (int|string)>|string>.

$sqlSelectExpressions[] = trim($e->dispatch($this));
break;

default:
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;
Expand Down
Loading

0 comments on commit 662a6cc

Please sign in to comment.