Skip to content

Commit 82c4243

Browse files
committed
add capability to use allfields sql notation
in a dto, this PR allow to call u.* to get all fileds fo u entity in one call,
1 parent 742eead commit 82c4243

File tree

8 files changed

+437
-16
lines changed

8 files changed

+437
-16
lines changed

Diff for: docs/en/reference/dql-doctrine-query-language.rst

+13-1
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,17 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
674674
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
675675
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.
676676

677+
In a Dto, if you want add all fields of an entity, you can use ``.*`` :
678+
.. code-block:: php
679+
680+
<?php
681+
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(a.*) AS address) FROM Customer c JOIN c.address a');
682+
$users = $query->getResult(); // array of CustomerDTO
683+
684+
// CustomerDTO => {name : 'DOE', email: null, city: null, address: {id: 18, city: 'New York', zip: '10011'}}
685+
686+
It's recommended to use named arguments DTOs with the ``.*`` notation because argument order is not guaranteed.
687+
677688
Using INDEX BY
678689
~~~~~~~~~~~~~~
679690

@@ -1702,7 +1713,8 @@ Select Expressions
17021713
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
17031714
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
17041715
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
1705-
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
1716+
NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
1717+
AllFieldsExpression ::= IdentificationVariable ".*"
17061718
17071719
Conditional Expressions
17081720
~~~~~~~~~~~~~~~~~~~~~~~

Diff for: phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -2739,7 +2739,7 @@ parameters:
27392739
-
27402740
message: '#^Cannot assign new offset to list\<string\>\|string\.$#'
27412741
identifier: offsetAssign.dimType
2742-
count: 2
2742+
count: 3
27432743
path: src/Query/SqlWalker.php
27442744

27452745
-

Diff for: src/Query/AST/AllFieldsExpression.php

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ORM\Query\AST;
6+
7+
use Doctrine\ORM\Query\SqlWalker;
8+
9+
/**
10+
* AllFieldsExpression ::= u.*
11+
*
12+
* @link www.doctrine-project.org
13+
*/
14+
class AllFieldsExpression extends Node
15+
{
16+
public string $field = '';
17+
18+
public function __construct(
19+
public string|null $identificationVariable,
20+
) {
21+
$this->field = $this->identificationVariable . '.*';
22+
}
23+
24+
public function dispatch(SqlWalker $walker, int|string $parent = '', int|string $argIndex = '', int|null &$aliasGap = null): string
25+
{
26+
return $walker->walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap);
27+
}
28+
}

Diff for: src/Query/AST/NewObjectExpression.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
*/
1717
class NewObjectExpression extends Node
1818
{
19+
public bool $hasNamedArgs = false;
20+
1921
/**
2022
* @param class-string $className
2123
* @param mixed[] $args
2224
*/
23-
public function __construct(public string $className, public array $args)
25+
public function __construct(public string $className, public array $args /*, public bool $hasNamedArgs = false */)
2426
{
27+
if (func_num_args() > 2) {
28+
$this->hasNamedArgs = func_get_arg(2);
29+
}
2530
}
2631

2732
public function dispatch(SqlWalker $walker /*, string|null $parentAlias = null */): string

Diff for: src/Query/Parser.php

+23-4
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,7 @@ public function PathExpression(int $expectedTypes): AST\PathExpression
10361036
assert($this->lexer->token !== null);
10371037
if ($this->lexer->isNextToken(TokenType::T_DOT)) {
10381038
$this->match(TokenType::T_DOT);
1039+
10391040
$this->match(TokenType::T_IDENTIFIER);
10401041

10411042
$field = $this->lexer->token->value;
@@ -1106,6 +1107,20 @@ public function CollectionValuedPathExpression(): AST\PathExpression
11061107
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
11071108
}
11081109

1110+
/**
1111+
* AllFieldsExpression ::= IdentificationVariable
1112+
*/
1113+
public function AllFieldsExpression(): AST\AllFieldsExpression
1114+
{
1115+
$identVariable = $this->IdentificationVariable();
1116+
assert($this->lexer->token !== null);
1117+
1118+
$this->match(TokenType::T_DOT);
1119+
$this->match(TokenType::T_MULTIPLY);
1120+
1121+
return new AST\AllFieldsExpression($identVariable);
1122+
}
1123+
11091124
/**
11101125
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
11111126
*/
@@ -1782,7 +1797,7 @@ public function NewObjectExpression(): AST\NewObjectExpression
17821797

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

1785-
$expression = new AST\NewObjectExpression($className, $args);
1800+
$expression = new AST\NewObjectExpression($className, $args, $useNamedArguments);
17861801

17871802
// Defer NewObjectExpression validation
17881803
$this->deferredNewObjectExpressions[] = [
@@ -1829,7 +1844,7 @@ public function addArgument(array &$args, bool $useNamedArguments): void
18291844
}
18301845

18311846
/**
1832-
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
1847+
* NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
18331848
*/
18341849
public function NewObjectArg(string|null &$fieldAlias = null): mixed
18351850
{
@@ -1939,10 +1954,14 @@ public function ScalarExpression(): mixed
19391954
// it is no function, so it must be a field path
19401955
case $lookahead === TokenType::T_IDENTIFIER:
19411956
$this->lexer->peek(); // lookahead => '.'
1942-
$this->lexer->peek(); // lookahead => token after '.'
1943-
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1957+
$token = $this->lexer->peek(); // lookahead => token after '.'
1958+
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
19441959
$this->lexer->resetPeek();
19451960

1961+
if ($token->value === '*') {
1962+
return $this->AllFieldsExpression();
1963+
}
1964+
19461965
if ($this->isMathOperator($peek)) {
19471966
return $this->SimpleArithmeticExpression();
19481967
}

Diff for: src/Query/SqlWalker.php

+59-9
Original file line numberDiff line numberDiff line change
@@ -1499,11 +1499,17 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
14991499
{
15001500
$sqlSelectExpressions = [];
15011501
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
1502+
$aliasGap = $newObjectExpression->hasNamedArgs ? null : 0;
15021503

15031504
foreach ($newObjectExpression->args as $argIndex => $e) {
1504-
$resultAlias = $this->scalarResultCounter++;
1505-
$columnAlias = $this->getSQLColumnAlias('sclr');
1506-
$fieldType = 'string';
1505+
if (! $newObjectExpression->hasNamedArgs) {
1506+
$argIndex += $aliasGap;
1507+
}
1508+
1509+
$resultAlias = $this->scalarResultCounter++;
1510+
$columnAlias = $this->getSQLColumnAlias('sclr');
1511+
$fieldType = 'string';
1512+
$isScalarResult = true;
15071513

15081514
switch (true) {
15091515
case $e instanceof AST\NewObjectExpression:
@@ -1549,18 +1555,26 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
15491555
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
15501556
break;
15511557

1558+
case $e instanceof AST\AllFieldsExpression:
1559+
$isScalarResult = false;
1560+
$sqlSelectExpressions[] = $e->dispatch($this, $objIndex, $argIndex, $aliasGap);
1561+
break;
1562+
15521563
default:
15531564
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
15541565
break;
15551566
}
15561567

1557-
$this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1558-
$this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
1568+
if ($isScalarResult) {
1569+
$this->scalarResultAliasMap[$resultAlias] = $columnAlias;
1570+
$this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
15591571

1560-
$this->rsm->newObjectMappings[$columnAlias] = [
1561-
'objIndex' => $objIndex,
1562-
'argIndex' => $argIndex,
1563-
];
1572+
$this->rsm->newObjectMappings[$columnAlias] = [
1573+
'className' => $newObjectExpression->className,
1574+
'objIndex' => $objIndex,
1575+
'argIndex' => $argIndex,
1576+
];
1577+
}
15641578
}
15651579

15661580
$this->rsm->newObject[$objIndex] = $newObjectExpression->className;
@@ -2265,6 +2279,42 @@ public function walkResultVariable(string $resultVariable): string
22652279
return $resultAlias;
22662280
}
22672281

2282+
public function walkAllEntityFieldsExpression(AST\AllFieldsExpression $expression, int|string $objIndex, int|string $argIndex, int|null &$aliasGap): string
2283+
{
2284+
$dqlAlias = $expression->identificationVariable;
2285+
$class = $this->getMetadataForDqlAlias($expression->identificationVariable);
2286+
2287+
$sqlParts = [];
2288+
// Select all fields from the queried class
2289+
foreach ($class->fieldMappings as $fieldName => $mapping) {
2290+
$tableName = isset($mapping->inherited)
2291+
? $this->em->getClassMetadata($mapping->inherited)->getTableName()
2292+
: $class->getTableName();
2293+
2294+
$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
2295+
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
2296+
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);
2297+
2298+
$col = $sqlTableAlias . '.' . $quotedColumnName;
2299+
2300+
$type = Type::getType($mapping->type);
2301+
$col = $type->convertToPHPValueSQL($col, $this->platform);
2302+
2303+
$sqlParts[] = $col . ' AS ' . $columnAlias;
2304+
2305+
$this->scalarResultAliasMap[$objIndex][] = $columnAlias;
2306+
2307+
$this->rsm->addScalarResult($columnAlias, $objIndex, $mapping->type);
2308+
2309+
$this->rsm->newObjectMappings[$columnAlias] = [
2310+
'objIndex' => $objIndex,
2311+
'argIndex' => $aliasGap === null ? $fieldName : (int) $argIndex + $aliasGap++,
2312+
];
2313+
}
2314+
2315+
return implode(', ', $sqlParts);
2316+
}
2317+
22682318
/**
22692319
* @return string The list in parentheses of valid child discriminators from the given class
22702320
*

Diff for: tests/Tests/Models/CMS/CmsDumbVariadicDTO.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\CMS;
6+
7+
class CmsDumbVariadicDTO
8+
{
9+
private array $values = [];
10+
11+
public function __construct(...$args)
12+
{
13+
foreach ($args as $key => $val) {
14+
$this->values[$key] = $val;
15+
}
16+
}
17+
18+
public function __get(string $key): mixed
19+
{
20+
return $this->values[$key] ?? null;
21+
}
22+
}

0 commit comments

Comments
 (0)