Skip to content

Commit e7705c2

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 e7705c2

File tree

8 files changed

+398
-22
lines changed

8 files changed

+398
-22
lines changed

docs/en/reference/dql-doctrine-query-language.rst

Lines changed: 13 additions & 1 deletion
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 AllFields notation :
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 Dto with AllFields 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
~~~~~~~~~~~~~~~~~~~~~~~

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
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
-

src/Query/AST/AllFieldsExpression.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 = 'fakefield';
17+
18+
public function __construct(
19+
public string|null $identificationVariable,
20+
) {
21+
}
22+
23+
public function dispatch(SqlWalker $walker, int|string $parent = '', int|string $argIndex = '', int|null &$aliasGap = null): string
24+
{
25+
return $walker->walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap);
26+
}
27+
}

src/Query/AST/NewObjectExpression.php

Lines changed: 6 additions & 1 deletion
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

src/Query/Parser.php

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,14 +1036,20 @@ 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-
$this->match(TokenType::T_IDENTIFIER);
1040-
1041-
$field = $this->lexer->token->value;
10421039

1043-
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
1044-
$this->match(TokenType::T_DOT);
1040+
if ($this->lexer->isNextToken(TokenType::T_MULTIPLY)) {
1041+
$this->match(TokenType::T_MULTIPLY);
1042+
$field = $this->lexer->token->value;
1043+
} else {
10451044
$this->match(TokenType::T_IDENTIFIER);
1046-
$field .= '.' . $this->lexer->token->value;
1045+
1046+
$field = $this->lexer->token->value;
1047+
1048+
while ($this->lexer->isNextToken(TokenType::T_DOT)) {
1049+
$this->match(TokenType::T_DOT);
1050+
$this->match(TokenType::T_IDENTIFIER);
1051+
$field .= '.' . $this->lexer->token->value;
1052+
}
10471053
}
10481054
}
10491055

@@ -1106,6 +1112,20 @@ public function CollectionValuedPathExpression(): AST\PathExpression
11061112
return $this->PathExpression(AST\PathExpression::TYPE_COLLECTION_VALUED_ASSOCIATION);
11071113
}
11081114

1115+
/**
1116+
* AllFieldsExpression ::= IdentificationVariable
1117+
*/
1118+
public function AllFieldsExpression(): AST\AllFieldsExpression
1119+
{
1120+
$identVariable = $this->IdentificationVariable();
1121+
assert($this->lexer->token !== null);
1122+
1123+
$this->match(TokenType::T_DOT);
1124+
$this->match(TokenType::T_MULTIPLY);
1125+
1126+
return new AST\AllFieldsExpression($identVariable);
1127+
}
1128+
11091129
/**
11101130
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
11111131
*/
@@ -1782,7 +1802,7 @@ public function NewObjectExpression(): AST\NewObjectExpression
17821802

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

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

17871807
// Defer NewObjectExpression validation
17881808
$this->deferredNewObjectExpressions[] = [
@@ -1829,7 +1849,7 @@ public function addArgument(array &$args, bool $useNamedArguments): void
18291849
}
18301850

18311851
/**
1832-
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
1852+
* NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
18331853
*/
18341854
public function NewObjectArg(string|null &$fieldAlias = null): mixed
18351855
{
@@ -1939,10 +1959,14 @@ public function ScalarExpression(): mixed
19391959
// it is no function, so it must be a field path
19401960
case $lookahead === TokenType::T_IDENTIFIER:
19411961
$this->lexer->peek(); // lookahead => '.'
1942-
$this->lexer->peek(); // lookahead => token after '.'
1943-
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
1962+
$token = $this->lexer->peek(); // lookahead => token after '.'
1963+
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
19441964
$this->lexer->resetPeek();
19451965

1966+
if ($token->value === '*') {
1967+
return $this->AllFieldsExpression();
1968+
}
1969+
19461970
if ($this->isMathOperator($peek)) {
19471971
return $this->SimpleArithmeticExpression();
19481972
}

src/Query/SqlWalker.php

Lines changed: 60 additions & 9 deletions
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,43 @@ public function walkResultVariable(string $resultVariable): string
22652279
return $resultAlias;
22662280
}
22672281

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