Skip to content

Commit

Permalink
Allow named Arguments to be passed to Dto
Browse files Browse the repository at this point in the history
Allow to change argument order or use variadic argument in dto constructor.
  • Loading branch information
eltharin committed Aug 20, 2024
1 parent 6f93ceb commit 2a83338
Show file tree
Hide file tree
Showing 14 changed files with 412 additions and 77 deletions.
32 changes: 32 additions & 0 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,38 @@ You can also nest several DTO :
Note that you can only pass scalar expressions or other Data Transfer Objects to the constructor.

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 :

.. code-block:: php
<?php
use Doctrine\ORM\WithNamedArguments;
class CustomerDTO implements WithNamedArguments
{
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $city = null, public mixed|null $value = null)
{
}
}
And then you can select the fields you want in order you want :

.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(a.city, c.name) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
you can either aliases column :

.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW CustomerDTO(c.name, CONCAT(a.city, ' ' , a.zip) AS value) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO
Using INDEX BY
~~~~~~~~~~~~~~

Expand Down
5 changes: 5 additions & 0 deletions phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,9 @@
<!-- https://github.com/doctrine/orm/issues/8537 -->
<exclude-pattern>src/QueryBuilder.php</exclude-pattern>
</rule>

<rule ref="Generic.Files.LineEndings.InvalidEOLChar">
<exclude name="Generic.Files.LineEndings.InvalidEOLChar"/>
</rule>

</ruleset>
4 changes: 2 additions & 2 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@
<code><![CDATA[return $rowData;]]></code>
</ReferenceConstraintViolation>
<PossiblyUndefinedArrayOffset>
<code><![CDATA[$newObject['args']]]></code>
<code><![CDATA[$newObject['args']]]></code>
<code><![CDATA[$newObject['argColumnNames']]]></code>
<code><![CDATA[$newObject['argColumnNames']]]></code>
</PossiblyUndefinedArrayOffset>
</file>
<file src="src/Internal/Hydration/ArrayHydrator.php">
Expand Down
62 changes: 42 additions & 20 deletions src/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\WithNamedArguments;
use Generator;
use LogicException;
use ReflectionClass;

use function array_key_exists;
use function array_map;
use function array_merge;
use function count;
Expand Down Expand Up @@ -254,14 +256,15 @@ abstract protected function hydrateAllData(): mixed;
* newObjects?: array<array-key, array{
* class: ReflectionClass,
* args: array,
* argNames?: array,
* obj: object
* }>,
* scalars?: array
* }
*/
protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
{
$rowData = ['data' => []];
$rowData = ['data' => [], 'newObjects' => []];

foreach ($data as $key => $value) {
$cacheKeyInfo = $this->hydrateColumnInfo($key);
Expand All @@ -282,12 +285,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;
$rowData['newObjects'][$objIndex]['argColumnNames'][$argIndex] = $key;

$rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
$rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
break;

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

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

$newObject = $rowData['newObjects'][$objIndex];
unset($rowData['newObjects'][$objIndex]);
$newObject = $rowData['newObjects'][$ownerIndex . ':' . $argIndex];
unset($rowData['newObjects'][$ownerIndex . ':' . $argIndex]);

$class = $newObject['class'];
$args = $newObject['args'];
$obj = $class->newInstanceArgs($args);
$class = $newObject['class'];
$args = $newObject['args'];
$argColumnNames = $newObject['argColumnNames'];
$obj = $this->getNewObjectInstance($class, $args, $argColumnNames);

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

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

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

return $rowData;
}

/**
* @param array<mixed> $args
* @param array<string> $argColumnNames
*/
private function getNewObjectInstance(ReflectionClass $class, array $args, array $argColumnNames): object
{
if ($class->implementsInterface(WithNamedArguments::class)) {
$newArgs = [];
foreach ($args as $key => $val) {
if (array_key_exists($key, $argColumnNames)) {
$newArgs[$this->resultSetMapping()->newObjectMappings[$argColumnNames[$key]]['objAlias']] = $val;
}
}

$args = $newArgs;
}

return $class->newInstanceArgs($args);
}

/**
* Processes a row of the result set.
*
Expand Down
7 changes: 5 additions & 2 deletions src/Query/AST/NewObjectExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
*/
class NewObjectExpression extends Node
{
/** @param mixed[] $args */
public function __construct(public string $className, public array $args)
/**
* @param array<mixed> $args
* @param array<?string> $argFieldAlias args key for named Arguments DTO
**/
public function __construct(public string $className, public array $args, public array $argFieldAlias)
{
}

Expand Down
33 changes: 23 additions & 10 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1734,25 +1734,30 @@ public function PartialObjectExpression(): AST\PartialObjectExpression
*/
public function NewObjectExpression(): AST\NewObjectExpression
{
$args = [];
$args = [];
$argFieldAlias = [];
$this->match(TokenType::T_NEW);

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

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

$args[] = $this->NewObjectArg();
$fieldAlias = null;
$args[] = $this->NewObjectArg($fieldAlias);
$argFieldAlias[] = $fieldAlias;

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

$args[] = $this->NewObjectArg();
$fieldAlias = null;
$args[] = $this->NewObjectArg($fieldAlias);
$argFieldAlias[] = $fieldAlias;
}

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

$expression = new AST\NewObjectExpression($className, $args);
$expression = new AST\NewObjectExpression($className, $args, $argFieldAlias);

// Defer NewObjectExpression validation
$this->deferredNewObjectExpressions[] = [
Expand All @@ -1767,26 +1772,34 @@ public function NewObjectExpression(): AST\NewObjectExpression
/**
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
*/
public function NewObjectArg(): mixed
public function NewObjectArg(string|null &$fieldAlias = null): mixed
{
$fieldAlias = null;

assert($this->lexer->lookahead !== null);
$token = $this->lexer->lookahead;
$peek = $this->lexer->glimpse();

assert($peek !== null);

$expression = null;

if ($token->type === TokenType::T_OPEN_PARENTHESIS && $peek->type === TokenType::T_SELECT) {
$this->match(TokenType::T_OPEN_PARENTHESIS);
$expression = $this->Subselect();
$this->match(TokenType::T_CLOSE_PARENTHESIS);

return $expression;
} elseif ($token->type === TokenType::T_NEW) {
$expression = $this->NewObjectExpression();
} else {
$expression = $this->ScalarExpression();
}

if ($token->type === TokenType::T_NEW) {
return $this->NewObjectExpression();
if ($this->lexer->isNextToken(TokenType::T_AS)) {
$this->match(TokenType::T_AS);
$fieldAlias = $this->AliasIdentificationVariable();
}

return $this->ScalarExpression();
return $expression;
}

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

namespace Doctrine\ORM\Query;

use function array_merge;
use function count;

/**
Expand Down Expand Up @@ -552,25 +551,4 @@ 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;
}
}
8 changes: 4 additions & 4 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1504,12 +1504,14 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
$resultAlias = $this->scalarResultCounter++;
$columnAlias = $this->getSQLColumnAlias('sclr');
$fieldType = 'string';
$objAlias = $newObjectExpression->argFieldAlias[$argIndex];

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

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

$type = Type::getType($fieldType);
$col = $type->convertToPHPValueSQL($col, $this->platform);
Expand Down Expand Up @@ -1562,11 +1565,8 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
'className' => $newObjectExpression->className,
'objIndex' => $objIndex,
'argIndex' => $argIndex,
'objAlias' => $objAlias,
];

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

return implode(', ', $sqlSelectExpressions);
Expand Down
9 changes: 9 additions & 0 deletions src/WithNamedArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM;

interface WithNamedArguments
{
}
14 changes: 14 additions & 0 deletions tests/Tests/Models/CMS/CmsAddressDTONamedArgs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\CMS;

use Doctrine\ORM\WithNamedArguments;

class CmsAddressDTONamedArgs implements WithNamedArguments
{
public function __construct(public string|null $country = null, public string|null $city = null, public string|null $zip = null, public CmsAddressDTO|string|null $address = null)
{
}
}
14 changes: 14 additions & 0 deletions tests/Tests/Models/CMS/CmsUserDTONamedArgs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\CMS;

use Doctrine\ORM\WithNamedArguments;

class CmsUserDTONamedArgs implements WithNamedArguments
{
public function __construct(public string|null $name = null, public string|null $email = null, public string|null $address = null, public int|null $phonenumbers = null, public CmsAddressDTO|null $addressDto = null, public CmsAddressDTONamedArgs|null $addressDtoNamedArgs = null)
{
}
}
23 changes: 23 additions & 0 deletions tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\CMS;

use Doctrine\ORM\WithNamedArguments;

class CmsUserDTOVariadicArg implements WithNamedArguments
{
public string|null $name = null;
public string|null $email = null;
public string|null $address = null;
public int|null $phonenumbers = null;

public function __construct(...$args)
{
$this->name = $args['name'] ?? null;
$this->email = $args['email'] ?? null;
$this->phonenumbers = $args['phonenumbers'] ?? null;
$this->address = $args['address'] ?? null;
}
}
Loading

0 comments on commit 2a83338

Please sign in to comment.