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 19, 2024
1 parent 1153b94 commit 35d8636
Show file tree
Hide file tree
Showing 9 changed files with 201 additions and 15 deletions.
8 changes: 5 additions & 3 deletions src/Internal/Hydration/AbstractHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ abstract protected function hydrateAllData(): mixed;
* data: array<array-key, array>,
* newObjects?: array<array-key, array{
* class: mixed,
* args?: array
* args?: array,
* argNames?: array,
* }>,
* scalars?: array
* }
Expand Down Expand Up @@ -281,8 +282,9 @@ protected function gatherRowData(array $data, array &$id, array &$nonemptyCompon
$value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
}

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

case isset($cacheKeyInfo['isScalar']):
Expand Down
18 changes: 15 additions & 3 deletions src/Internal/Hydration/ObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\WithNamedArguments;

use function array_fill_keys;
use function array_keys;
Expand Down Expand Up @@ -556,9 +557,20 @@ 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);
$class = $newObject['class'];
$args = $newObject['args'];
$argNames = $newObject['argNames'];

Check failure on line 562 in src/Internal/Hydration/ObjectHydrator.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (default)

PossiblyUndefinedArrayOffset

src/Internal/Hydration/ObjectHydrator.php:562:29: PossiblyUndefinedArrayOffset: Possibly undefined array key $newObject['argNames'] on array{argNames?: array<array-key, mixed>, args?: array<array-key, mixed>, class: mixed} (see https://psalm.dev/167)

Check failure on line 562 in src/Internal/Hydration/ObjectHydrator.php

View workflow job for this annotation

GitHub Actions / Static Analysis with Psalm (3.8.2)

PossiblyUndefinedArrayOffset

src/Internal/Hydration/ObjectHydrator.php:562:29: PossiblyUndefinedArrayOffset: Possibly undefined array key $newObject['argNames'] on array{argNames?: array<array-key, mixed>, args?: array<array-key, mixed>, class: mixed} (see https://psalm.dev/167)

if ($class->implementsInterface(WithNamedArguments::class)) {
$newArgs = [];
foreach ($args as $key => $val) {
$newArgs[$this->resultSetMapping()->newObjectMappings[$argNames[$key]]['argName']] = $val;
}

$args = $newArgs;
}

$obj = $class->newInstanceArgs($args);

if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
$result[$resultKey] = $obj;
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> $argNames args key for named Arguments DTO
**/
public function __construct(public string $className, public array $args, public array $argNames)
{
}

Expand Down
29 changes: 22 additions & 7 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1626,25 +1626,30 @@ public function JoinAssociationDeclaration(): AST\JoinAssociationDeclaration
*/
public function NewObjectExpression(): AST\NewObjectExpression
{
$args = [];
$args = [];
$argNames = [];

$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();
$argName = null;
$args[] = $this->NewObjectArg($argName);
$argNames[] = $argName;

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

$args[] = $this->NewObjectArg();
$args[] = $this->NewObjectArg($argName);
$argNames[] = $argName;
}

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

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

// Defer NewObjectExpression validation
$this->deferredNewObjectExpressions[] = [
Expand All @@ -1659,22 +1664,32 @@ public function NewObjectExpression(): AST\NewObjectExpression
/**
* NewObjectArg ::= ScalarExpression | "(" Subselect ")"
*/
public function NewObjectArg(): mixed
public function NewObjectArg(string|null &$argName = null): mixed
{
$argName = 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);
} else {
$expression = $this->ScalarExpression();
}

return $expression;
if ($this->lexer->isNextToken(TokenType::T_AS)) {
$this->match(TokenType::T_AS);
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
$argName = $aliasIdentificationVariable;
}

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

/**
Expand Down
3 changes: 3 additions & 0 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
$resultAlias = $this->scalarResultCounter++;
$columnAlias = $this->getSQLColumnAlias('sclr');
$fieldType = 'string';
$argName = $newObjectExpression->argNames[$argIndex];

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

$type = Type::getType($fieldType);
$col = $type->convertToPHPValueSQL($col, $this->platform);
Expand Down Expand Up @@ -1523,6 +1525,7 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
'className' => $newObjectExpression->className,
'objIndex' => $objIndex,
'argIndex' => $argIndex,
'argName' => $argName,
];
}

Expand Down
12 changes: 12 additions & 0 deletions src/WithNamedArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Doctrine\ORM;

/**
* Interface for allow passing named arguments to Dto with NEW operator.
*/
interface WithNamedArguments
{
}
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)
{
}
}
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;

Check failure on line 11 in tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Equals sign not aligned with surrounding assignments; expected 9 spaces but found 1 space
public string|null $email = null;

Check failure on line 12 in tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Equals sign not aligned with surrounding assignments; expected 8 spaces but found 1 space
public string|null $address = null;

Check failure on line 13 in tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

Equals sign not aligned with surrounding assignments; expected 6 spaces but found 1 space
public int|null $phonenumbers = null;

Check failure on line 14 in tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

There must be exactly one space between type hint and property $phonenumbers.

Check failure on line 14 in tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php

View workflow job for this annotation

GitHub Actions / coding-standards / Coding Standards (8.3)

There must be 1 space after the property type declaration; 4 found

public function __construct(...$args)
{
$this->name = $args['name'] ?? null;
$this->email = $args['email'] ?? null;
$this->phonenumbers = $args['phonenumbers'] ?? null;
$this->address = $args['address'] ?? null;
}
}
102 changes: 102 additions & 0 deletions tests/Tests/ORM/Functional/NewOperatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use Doctrine\Tests\Models\CMS\CmsPhonenumber;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\Models\CMS\CmsUserDTO;
use Doctrine\Tests\Models\CMS\CmsUserDTONamedArgs;
use Doctrine\Tests\Models\CMS\CmsUserDTOVariadicArg;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
Expand Down Expand Up @@ -1013,6 +1015,106 @@ public function testClassCantBeInstantiatedException(): void
$dql = 'SELECT new Doctrine\Tests\ORM\Functional\ClassWithPrivateConstructor(u.name) FROM Doctrine\Tests\Models\CMS\CmsUser u';
$this->_em->createQuery($dql)->getResult();
}

public function testNamedArguments(): void
{
$dql = '
SELECT
new CmsUserDTONamedArgs(
e.email,
u.name,
CONCAT(a.country, \' \', a.city, \' \', a.zip) AS address
) as user,
u.status,
u.username as cmsUserUsername
FROM
Doctrine\Tests\Models\CMS\CmsUser u
JOIN
u.email e
JOIN
u.address a
ORDER BY
u.name';

$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();

self::assertCount(3, $result);

self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]['user']);
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]['user']);
self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]['user']);

self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);

self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);

self::assertSame($this->fixtures[0]->address->country . ' ' . $this->fixtures[0]->address->city . ' ' . $this->fixtures[0]->address->zip, $result[0]['user']->address);
self::assertSame($this->fixtures[1]->address->country . ' ' . $this->fixtures[1]->address->city . ' ' . $this->fixtures[1]->address->zip, $result[1]['user']->address);
self::assertSame($this->fixtures[2]->address->country . ' ' . $this->fixtures[2]->address->city . ' ' . $this->fixtures[2]->address->zip, $result[2]['user']->address);

self::assertSame($this->fixtures[0]->status, $result[0]['status']);
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
self::assertSame($this->fixtures[2]->status, $result[2]['status']);

self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
}

public function testVariadicArgument(): void
{
$dql = '
SELECT
new CmsUserDTOVariadicArg(
CONCAT(a.country, \' \', a.city, \' \', a.zip) AS address,
e.email,
u.name
) as user,
u.status,
u.username as cmsUserUsername
FROM
Doctrine\Tests\Models\CMS\CmsUser u
JOIN
u.email e
JOIN
u.address a
ORDER BY
u.name';

$query = $this->getEntityManager()->createQuery($dql);
$result = $query->getResult();

self::assertCount(3, $result);

self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]['user']);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]['user']);
self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]['user']);

self::assertSame($this->fixtures[0]->name, $result[0]['user']->name);
self::assertSame($this->fixtures[1]->name, $result[1]['user']->name);
self::assertSame($this->fixtures[2]->name, $result[2]['user']->name);

self::assertSame($this->fixtures[0]->email->email, $result[0]['user']->email);
self::assertSame($this->fixtures[1]->email->email, $result[1]['user']->email);
self::assertSame($this->fixtures[2]->email->email, $result[2]['user']->email);

self::assertSame($this->fixtures[0]->address->country . ' ' . $this->fixtures[0]->address->city . ' ' . $this->fixtures[0]->address->zip, $result[0]['user']->address);
self::assertSame($this->fixtures[1]->address->country . ' ' . $this->fixtures[1]->address->city . ' ' . $this->fixtures[1]->address->zip, $result[1]['user']->address);
self::assertSame($this->fixtures[2]->address->country . ' ' . $this->fixtures[2]->address->city . ' ' . $this->fixtures[2]->address->zip, $result[2]['user']->address);

self::assertSame($this->fixtures[0]->status, $result[0]['status']);
self::assertSame($this->fixtures[1]->status, $result[1]['status']);
self::assertSame($this->fixtures[2]->status, $result[2]['status']);

self::assertSame($this->fixtures[0]->username, $result[0]['cmsUserUsername']);
self::assertSame($this->fixtures[1]->username, $result[1]['cmsUserUsername']);
self::assertSame($this->fixtures[2]->username, $result[2]['cmsUserUsername']);
}
}

class ClassWithTooMuchArgs
Expand Down

0 comments on commit 35d8636

Please sign in to comment.