Skip to content

Commit

Permalink
Merge pull request #11576 from eltharin/newnestedDto
Browse files Browse the repository at this point in the history
Re: Re: Add support for using nested DTOs
  • Loading branch information
greg0ire authored Aug 19, 2024
2 parents 5f1fe15 + 8c582a4 commit 6f93ceb
Show file tree
Hide file tree
Showing 16 changed files with 402 additions and 18 deletions.
29 changes: 28 additions & 1 deletion docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,34 @@ And then use the ``NEW`` DQL keyword :
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, a.city, SUM(o.value)) FROM Customer c JOIN c.email e JOIN c.address a JOIN c.orders o GROUP BY c');
$users = $query->getResult(); // array of CustomerDTO
Note that you can only pass scalar expressions to the constructor.
You can also nest several DTO :

.. code-block:: php
<?php
class CustomerDTO
{
public function __construct(string $name, string $email, AddressDTO $address, string|null $value = null)
{
// Bind values to the object properties.
}
}
class AddressDTO
{
public function __construct(string $street, string $city, string $zip)
{
// Bind values to the object properties.
}
}
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, e.email, NEW AddressDTO(a.street, a.city, a.zip)) FROM Customer c JOIN c.email e JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.

Using INDEX BY
~~~~~~~~~~~~~~
Expand Down
10 changes: 4 additions & 6 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,10 @@
<code><![CDATA[return $rowData;]]></code>
<code><![CDATA[return $rowData;]]></code>
</ReferenceConstraintViolation>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/Internal/Hydration/ArrayHydrator.php">
<PossiblyInvalidArgument>
Expand All @@ -228,9 +232,6 @@
<code><![CDATA[$result[$resultKey]]]></code>
<code><![CDATA[$result[$resultKey]]]></code>
</PossiblyNullArrayAssignment>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
<ReferenceConstraintViolation>
<code><![CDATA[$result]]></code>
</ReferenceConstraintViolation>
Expand Down Expand Up @@ -265,9 +266,6 @@
<code><![CDATA[setValue]]></code>
<code><![CDATA[setValue]]></code>
</PossiblyNullReference>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/Mapping/AssociationMapping.php">
<LessSpecificReturnStatement>
Expand Down
34 changes: 32 additions & 2 deletions src/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ abstract protected function hydrateAllData(): mixed;
* @psalm-return array{
* data: array<array-key, array>,
* newObjects?: array<array-key, array{
* class: mixed,
* args?: array
* class: ReflectionClass,
* args: array,
* obj: object
* }>,
* scalars?: array
* }
Expand Down Expand Up @@ -281,6 +282,10 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
}

if (! isset($rowData['newObjects'])) {
$rowData['newObjects'] = [];
}

$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
break;
Expand Down Expand Up @@ -335,6 +340,31 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
}
}

foreach ($this->resultSetMapping()->nestedNewObjectArguments as $objIndex => ['ownerIndex' => $ownerIndex, 'argIndex' => $argIndex]) {
if (! isset($rowData['newObjects'][$objIndex])) {
continue;
}

$newObject = $rowData['newObjects'][$objIndex];
unset($rowData['newObjects'][$objIndex]);

$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);

$rowData['newObjects'][$ownerIndex]['args'][$argIndex] = $obj;
}

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

$rowData['newObjects'][$objIndex]['obj'] = $obj;
}
}

return $rowData;
}

Expand Down
5 changes: 2 additions & 3 deletions src/Internal/Hydration/ArrayHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,8 @@ protected function hydrateRowData(array $row, array &$result): void
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);

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

if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
$result[$resultKey] = $obj;
Expand Down
4 changes: 1 addition & 3 deletions src/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -556,9 +556,7 @@ protected function hydrateRowData(array $row, array &$result): void
$scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);

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

if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
$result[$resultKey] = $obj;
Expand Down
4 changes: 4 additions & 0 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,10 @@ public function NewObjectArg(): mixed
return $expression;
}

if ($token->type === TokenType::T_NEW) {
return $this->NewObjectExpression();
}

return $this->ScalarExpression();
}

Expand Down
29 changes: 29 additions & 0 deletions src/Query/ResultSetMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\ORM\Query;

use function array_merge;
use function count;

/**
Expand Down Expand Up @@ -152,6 +153,13 @@ class ResultSetMapping
*/
public array $newObjectMappings = [];

/**
* Maps last argument for new objects in order to initiate object construction
*
* @psalm-var array<int|string, array{ownerIndex: string|int, argIndex: int|string}>
*/
public array $nestedNewObjectArguments = [];

/**
* Maps metadata parameter names to the metadata attribute.
*
Expand Down Expand Up @@ -544,4 +552,25 @@ public function addMetaResult(

return $this;
}

public function addNewObjectAsArgument(string|int $alias, string|int $objOwner, int $objOwnerIdx): static
{
$owner = [
'ownerIndex' => $objOwner,
'argIndex' => $objOwnerIdx,
];

if (! isset($this->nestedNewObjectArguments[$owner['ownerIndex']])) {
$this->nestedNewObjectArguments[$alias] = $owner;

return $this;
}

$this->nestedNewObjectArguments = array_merge(
[$alias => $owner],
$this->nestedNewObjectArguments,
);

return $this;
}
}
24 changes: 23 additions & 1 deletion src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@
use function array_keys;
use function array_map;
use function array_merge;
use function array_pop;
use function assert;
use function count;
use function end;
use function implode;
use function in_array;
use function is_array;
Expand Down Expand Up @@ -85,6 +87,13 @@ class SqlWalker
*/
private int $newObjectCounter = 0;

/**
* Contains nesting levels of new objects arguments
*
* @psalm-var array<int, array{0: string|int, 1: int}>
*/
private array $newObjectStack = [];

private readonly EntityManagerInterface $em;
private readonly Connection $conn;

Expand Down Expand Up @@ -1482,7 +1491,14 @@ public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesis
public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string
{
$sqlSelectExpressions = [];
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
$objOwner = $objOwnerIdx = null;

if ($this->newObjectStack !== []) {
[$objOwner, $objOwnerIdx] = end($this->newObjectStack);
$objIndex = $objOwner . ':' . $objOwnerIdx;
} else {
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
}

foreach ($newObjectExpression->args as $argIndex => $e) {
$resultAlias = $this->scalarResultCounter++;
Expand All @@ -1491,7 +1507,9 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri

switch (true) {
case $e instanceof AST\NewObjectExpression:
$this->newObjectStack[] = [$objIndex, $argIndex];
$sqlSelectExpressions[] = $e->dispatch($this);
array_pop($this->newObjectStack);
break;

case $e instanceof AST\Subselect:
Expand Down Expand Up @@ -1545,6 +1563,10 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];

if ($objOwner !== null && $objOwnerIdx !== null) {
$this->rsm->addNewObjectAsArgument($objIndex, $objOwner, $objOwnerIdx);
}
}

return implode(', ', $sqlSelectExpressions);
Expand Down
2 changes: 1 addition & 1 deletion tests/Tests/Models/CMS/CmsAddressDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class CmsAddressDTO
{
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null)
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null, public CmsAddressDTO|string|null $address = null)
{
}
}
2 changes: 1 addition & 1 deletion tests/Tests/Models/CMS/CmsUserDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

class CmsUserDTO
{
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $address = null, public int|null $phonenumbers = null)
public function __construct(public string|null $name = null, public string|null $email = null, public CmsAddressDTO|string|null $address = null, public int|null $phonenumbers = null)
{
}
}
17 changes: 17 additions & 0 deletions tests/Tests/Models/DDC6573/DDC6573Currency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DDC6573;

final class DDC6573Currency
{
public function __construct(private readonly string $code)
{
}

public function getCode(): string
{
return $this->code;
}
}
44 changes: 44 additions & 0 deletions tests/Tests/Models/DDC6573/DDC6573Item.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DDC6573;

use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Table;

#[Entity]
#[Table(name: 'ddc6573_items')]
class DDC6573Item
{
/** @var int */
#[Id]
#[Column(type: Types::INTEGER)]
#[GeneratedValue(strategy: 'AUTO')]
public $id;

#[Column(type: Types::STRING)]
public string $name;

#[Column(type: Types::INTEGER)]
public int $priceAmount;

#[Column(type: Types::STRING, length: 3)]
public string $priceCurrency;

public function __construct(string $name, DDC6573Money $price)
{
$this->name = $name;
$this->priceAmount = $price->getAmount();
$this->priceCurrency = $price->getCurrency()->getCode();
}

public function getPrice(): DDC6573Money
{
return new DDC6573Money($this->priceAmount, new DDC6573Currency($this->priceCurrency));
}
}
24 changes: 24 additions & 0 deletions tests/Tests/Models/DDC6573/DDC6573Money.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\DDC6573;

final class DDC6573Money
{
public function __construct(
private readonly int $amount,
private readonly DDC6573Currency $currency,
) {
}

public function getAmount(): int
{
return $this->amount;
}

public function getCurrency(): DDC6573Currency
{
return $this->currency;
}
}
Loading

0 comments on commit 6f93ceb

Please sign in to comment.