Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,18 @@ You can hydrate an entity nested in a DTO :

// CustomerDTO => {name : 'DOE', email: null, address : {city: 'New York', zip: '10011', address: 'Abbey Road'}

In a DTO, if you want to add all fields of an entity, you can use ``ALLFIELDS`` :

.. code-block:: php

<?php
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(ALLFIELDS(a)) AS address) FROM Customer c JOIN c.address a');
$users = $query->getResult(); // array of CustomerDTO

// CustomerDTO => {name : 'DOE', email: null, city: null, address: {id: 18, city: 'New York', zip: '10011'}}

It's mandatory to use named arguments DTOs with the ``ALLFIELDS`` notation because argument order is not guaranteed, otherwise an exception will be thrown.

Using INDEX BY
~~~~~~~~~~~~~~

Expand Down Expand Up @@ -1707,13 +1719,14 @@ Select Expressions

.. code-block:: php

SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
SelectExpression ::= (IdentificationVariable | ScalarExpression | AggregateExpression | FunctionDeclaration | PartialObjectExpression | "(" Subselect ")" | CaseExpression | NewObjectExpression) [["AS"] ["HIDDEN"] AliasResultVariable]
SimpleSelectExpression ::= (StateFieldPathExpression | IdentificationVariable | FunctionDeclaration | AggregateExpression | "(" Subselect ")" | ScalarExpression) [["AS"] AliasResultVariable]
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]
NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression | EntityAsDtoArgumentExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
EntityAsDtoArgumentExpression ::= IdentificationVariable
AllFieldsExpression ::= "ALLFIELDS(" IdentificationVariable ")"

Conditional Expressions
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2811,7 +2811,7 @@ parameters:
-
message: '#^Cannot assign new offset to list\<string\>\|string\.$#'
identifier: offsetAssign.dimType
count: 2
count: 3
path: src/Query/SqlWalker.php

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

declare(strict_types=1);

namespace Doctrine\ORM\Query\AST;

use Doctrine\ORM\Query\SqlWalker;

/**
* AllFieldsExpression ::= u.*
*
* @link www.doctrine-project.org
*/
class AllFieldsExpression extends Node
{
public string $field = '';

public function __construct(
public string|null $identificationVariable,
) {
$this->field = $this->identificationVariable . '.*';
}

public function dispatch(SqlWalker $walker, int|string $parent = '', int|string $argIndex = '', int|null &$aliasGap = null): string
{
return $walker->walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap);
}
}
2 changes: 1 addition & 1 deletion src/Query/AST/NewObjectExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class NewObjectExpression extends Node
* @param class-string $className
* @param mixed[] $args
*/
public function __construct(public string $className, public array $args)
public function __construct(public string $className, public array $args, public bool $hasNamedArgs = false)
{
}

Expand Down
25 changes: 21 additions & 4 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,21 @@ public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpressi
return new AST\EntityAsDtoArgumentExpression($expression, $identVariable);
}

/**
* AllFieldsExpression ::= "ALLFIELDS(" IdentificationVariable ")"
*/
public function AllFieldsExpression(): AST\AllFieldsExpression
{
assert($this->lexer->token !== null);

$this->match(TokenType::T_ALLFIELDS);
$this->match(TokenType::T_OPEN_PARENTHESIS);
$identVariable = $this->IdentificationVariable();
$this->match(TokenType::T_CLOSE_PARENTHESIS);

return new AST\AllFieldsExpression($identVariable);
}

/**
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
*/
Expand Down Expand Up @@ -1826,7 +1841,7 @@ public function NewObjectExpression(): AST\NewObjectExpression

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

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

// Defer NewObjectExpression validation
$this->deferredNewObjectExpressions[] = [
Expand Down Expand Up @@ -1873,7 +1888,7 @@ public function addArgument(array &$args, bool $useNamedArguments): void
}

/**
* NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
* NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
*/
public function NewObjectArg(string|null &$fieldAlias = null): mixed
{
Expand All @@ -1893,6 +1908,8 @@ public function NewObjectArg(string|null &$fieldAlias = null): mixed
$this->match(TokenType::T_CLOSE_PARENTHESIS);
} elseif ($token->type === TokenType::T_NEW) {
$expression = $this->NewObjectExpression();
} elseif ($token->type === TokenType::T_ALLFIELDS) {
$expression = $this->AllFieldsExpression();
} elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) {
$expression = $this->EntityAsDtoArgumentExpression();
} else {
Expand Down Expand Up @@ -1985,8 +2002,8 @@ public function ScalarExpression(): mixed
// it is no function, so it must be a field path
case $lookahead === TokenType::T_IDENTIFIER:
$this->lexer->peek(); // lookahead => '.'
$this->lexer->peek(); // lookahead => token after '.'
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
$token = $this->lexer->peek(); // lookahead => token after '.'
$peek = $this->lexer->peek(); // lookahead => token after the token after the '.'
$this->lexer->resetPeek();

if ($this->isMathOperator($peek)) {
Expand Down
79 changes: 70 additions & 9 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,9 @@
$sql .= $this->walkNewObject($expr, $selectExpression->fieldIdentificationVariable);
break;

case $expr instanceof AST\AllFieldsExpression:
throw new LogicException('All fields Expression are only supported in DTO.');

Check warning on line 1356 in src/Query/SqlWalker.php

View check run for this annotation

Codecov / codecov/patch

src/Query/SqlWalker.php#L1356

Added line #L1356 was not covered by tests

default:
// IdentificationVariable or PartialObjectExpression
if ($expr instanceof AST\PartialObjectExpression) {
Expand Down Expand Up @@ -1518,11 +1521,17 @@
{
$sqlSelectExpressions = [];
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
$aliasGap = $newObjectExpression->hasNamedArgs ? null : 0;

foreach ($newObjectExpression->args as $argIndex => $e) {
$resultAlias = $this->scalarResultCounter++;
$columnAlias = $this->getSQLColumnAlias('sclr');
$fieldType = 'string';
if (! $newObjectExpression->hasNamedArgs) {
$argIndex += $aliasGap;
}

$resultAlias = $this->scalarResultCounter++;
$columnAlias = $this->getSQLColumnAlias('sclr');
$fieldType = 'string';
$isScalarResult = true;

switch (true) {
case $e instanceof AST\NewObjectExpression:
Expand Down Expand Up @@ -1576,18 +1585,30 @@
$sqlSelectExpressions[] = trim($e->dispatch($this));
break;

case $e instanceof AST\AllFieldsExpression:
if (! $newObjectExpression->hasNamedArgs) {
throw new LogicException('All fields Expression must be used with named arguments DTO constructor.');
}

$isScalarResult = false;
$sqlSelectExpressions[] = $e->dispatch($this, $objIndex, $argIndex, $aliasGap);
break;

default:
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;
}

$this->scalarResultAliasMap[$resultAlias] = $columnAlias;
$this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);
if ($isScalarResult) {
$this->scalarResultAliasMap[$resultAlias] = $columnAlias;
$this->rsm->addScalarResult($columnAlias, $resultAlias, $fieldType);

$this->rsm->newObjectMappings[$columnAlias] = [
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];
$this->rsm->newObjectMappings[$columnAlias] = [
'className' => $newObjectExpression->className,
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];
}
}

$this->rsm->newObject[$objIndex] = $newObjectExpression->className;
Expand Down Expand Up @@ -2292,6 +2313,46 @@
return $resultAlias;
}

public function walkAllEntityFieldsExpression(AST\AllFieldsExpression $expression, int|string $objIndex, int|string $argIndex, int|null &$aliasGap): string
{
$dqlAlias = $expression->identificationVariable;
$class = $this->getMetadataForDqlAlias($expression->identificationVariable);

$sqlParts = [];
// Select all fields from the queried class
foreach ($class->fieldMappings as $fieldName => $mapping) {
$tableName = isset($mapping->inherited)
? $this->em->getClassMetadata($mapping->inherited)->getTableName()

Check warning on line 2325 in src/Query/SqlWalker.php

View check run for this annotation

Codecov / codecov/patch

src/Query/SqlWalker.php#L2325

Added line #L2325 was not covered by tests
: $class->getTableName();

$sqlTableAlias = $this->getSQLTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSQLColumnAlias($mapping->columnName);
$quotedColumnName = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform);

$col = $sqlTableAlias . '.' . $quotedColumnName;

$type = Type::getType($mapping->type);
$col = $type->convertToPHPValueSQL($col, $this->platform);

$sqlParts[] = $col . ' AS ' . $columnAlias;

$this->scalarResultAliasMap[$objIndex][] = $columnAlias;

$this->rsm->addScalarResult($columnAlias, $objIndex, $mapping->type);

if ($mapping->enumType !== null) {
$this->rsm->addEnumResult($columnAlias, $mapping->enumType);

Check warning on line 2344 in src/Query/SqlWalker.php

View check run for this annotation

Codecov / codecov/patch

src/Query/SqlWalker.php#L2344

Added line #L2344 was not covered by tests
}

$this->rsm->newObjectMappings[$columnAlias] = [
'objIndex' => $objIndex,
'argIndex' => $aliasGap === null ? $fieldName : (int) $argIndex + $aliasGap++,
];
}

return implode(', ', $sqlParts);
}

/**
* @return string The list in parentheses of valid child discriminators from the given class
*
Expand Down
117 changes: 59 additions & 58 deletions src/Query/TokenType.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,62 +32,63 @@ enum TokenType: int
case T_IDENTIFIER = 102;

// All keyword tokens should be >= 200
case T_ALL = 200;
case T_AND = 201;
case T_ANY = 202;
case T_AS = 203;
case T_ASC = 204;
case T_AVG = 205;
case T_BETWEEN = 206;
case T_BOTH = 207;
case T_BY = 208;
case T_CASE = 209;
case T_COALESCE = 210;
case T_COUNT = 211;
case T_DELETE = 212;
case T_DESC = 213;
case T_DISTINCT = 214;
case T_ELSE = 215;
case T_EMPTY = 216;
case T_END = 217;
case T_ESCAPE = 218;
case T_EXISTS = 219;
case T_FALSE = 220;
case T_FROM = 221;
case T_GROUP = 222;
case T_HAVING = 223;
case T_HIDDEN = 224;
case T_IN = 225;
case T_INDEX = 226;
case T_INNER = 227;
case T_INSTANCE = 228;
case T_IS = 229;
case T_JOIN = 230;
case T_LEADING = 231;
case T_LEFT = 232;
case T_LIKE = 233;
case T_MAX = 234;
case T_MEMBER = 235;
case T_MIN = 236;
case T_NEW = 237;
case T_NOT = 238;
case T_NULL = 239;
case T_NULLIF = 240;
case T_OF = 241;
case T_OR = 242;
case T_ORDER = 243;
case T_OUTER = 244;
case T_PARTIAL = 245;
case T_SELECT = 246;
case T_SET = 247;
case T_SOME = 248;
case T_SUM = 249;
case T_THEN = 250;
case T_TRAILING = 251;
case T_TRUE = 252;
case T_UPDATE = 253;
case T_WHEN = 254;
case T_WHERE = 255;
case T_WITH = 256;
case T_NAMED = 257;
case T_ALL = 200;
case T_AND = 201;
case T_ANY = 202;
case T_AS = 203;
case T_ASC = 204;
case T_AVG = 205;
case T_BETWEEN = 206;
case T_BOTH = 207;
case T_BY = 208;
case T_CASE = 209;
case T_COALESCE = 210;
case T_COUNT = 211;
case T_DELETE = 212;
case T_DESC = 213;
case T_DISTINCT = 214;
case T_ELSE = 215;
case T_EMPTY = 216;
case T_END = 217;
case T_ESCAPE = 218;
case T_EXISTS = 219;
case T_FALSE = 220;
case T_FROM = 221;
case T_GROUP = 222;
case T_HAVING = 223;
case T_HIDDEN = 224;
case T_IN = 225;
case T_INDEX = 226;
case T_INNER = 227;
case T_INSTANCE = 228;
case T_IS = 229;
case T_JOIN = 230;
case T_LEADING = 231;
case T_LEFT = 232;
case T_LIKE = 233;
case T_MAX = 234;
case T_MEMBER = 235;
case T_MIN = 236;
case T_NEW = 237;
case T_NOT = 238;
case T_NULL = 239;
case T_NULLIF = 240;
case T_OF = 241;
case T_OR = 242;
case T_ORDER = 243;
case T_OUTER = 244;
case T_PARTIAL = 245;
case T_SELECT = 246;
case T_SET = 247;
case T_SOME = 248;
case T_SUM = 249;
case T_THEN = 250;
case T_TRAILING = 251;
case T_TRUE = 252;
case T_UPDATE = 253;
case T_WHEN = 254;
case T_WHERE = 255;
case T_WITH = 256;
case T_NAMED = 257;
case T_ALLFIELDS = 258;
}
Loading