Skip to content

Commit

Permalink
add capability to use allfields sql notation
Browse files Browse the repository at this point in the history
in a dto, this PR allow to call u.* to get all fileds fo u entity in one call,
  • Loading branch information
eltharin committed Feb 21, 2025
1 parent 8873109 commit 6d9c4aa
Show file tree
Hide file tree
Showing 7 changed files with 510 additions and 24 deletions.
14 changes: 13 additions & 1 deletion docs/en/reference/dql-doctrine-query-language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,17 @@ The ``NAMED`` keyword must precede all DTO you want to instantiate :
If two arguments have the same name, a ``DuplicateFieldException`` is thrown.
If a field cannot be matched with a property name, a ``NoMatchingPropertyException`` is thrown. This typically happens when using functions without aliasing them.

In a Dto, if you want add all fields of an entity, you can use AllFields notation :
.. code-block:: php
<?php
$query = $em->createQuery('SELECT NEW NAMED CustomerDTO(c.name, NEW NAMED AddressDTO(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 recommended to use named arguments Dto with AllFields notation because argument order is not guaranteed.

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

Expand Down Expand Up @@ -1702,7 +1713,8 @@ Select Expressions
PartialObjectExpression ::= "PARTIAL" IdentificationVariable "." PartialFieldSet
PartialFieldSet ::= "{" SimpleStateField {"," SimpleStateField}* "}"
NewObjectExpression ::= "NEW" AbstractSchemaName "(" NewObjectArg {"," NewObjectArg}* ")"
NewObjectArg ::= (ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]
NewObjectArg ::= ((ScalarExpression | "(" Subselect ")" | NewObjectExpression) ["AS" AliasResultVariable]) | AllFieldsExpression
AllFieldsExpression ::= IdentificationVariable ".*"
Conditional Expressions
~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
27 changes: 27 additions & 0 deletions src/Query/AST/AllFieldsExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?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 = 'fakefield';

public function __construct(
public string|null $identificationVariable,
) {
}

public function dispatch(SqlWalker $walker, int|string $parent = '', int|string $argIndex = '', int|null &$aliasGap = null): string
{
return $walker->walkAllEntityFieldsExpression($this, $parent, $argIndex, $aliasGap);
}
}
15 changes: 13 additions & 2 deletions src/Query/AST/NewObjectExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,27 @@

use Doctrine\ORM\Query\SqlWalker;

use function func_get_arg;
use function func_num_args;

/**
* NewObjectExpression ::= "NEW" IdentificationVariable "(" NewObjectArg {"," NewObjectArg}* ")"
*
* @link www.doctrine-project.org
*/
class NewObjectExpression extends Node
{
/** @param mixed[] $args */
public function __construct(public string $className, public array $args)
public bool $hasNamedArgs = false;

/**
* @param class-string $className
* @param mixed[] $args
*/
public function __construct(public string $className, public array $args /*, public bool $hasNamedArgs = false */)
{
if (func_num_args() > 2) {
$this->hasNamedArgs = func_get_arg(2);
}
}

public function dispatch(SqlWalker $walker): string
Expand Down
44 changes: 34 additions & 10 deletions src/Query/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -1036,14 +1036,20 @@ public function PathExpression(int $expectedTypes): AST\PathExpression
assert($this->lexer->token !== null);
if ($this->lexer->isNextToken(TokenType::T_DOT)) {
$this->match(TokenType::T_DOT);
$this->match(TokenType::T_IDENTIFIER);

$field = $this->lexer->token->value;

while ($this->lexer->isNextToken(TokenType::T_DOT)) {
$this->match(TokenType::T_DOT);
if ($this->lexer->isNextToken(TokenType::T_MULTIPLY)) {
$this->match(TokenType::T_MULTIPLY);
$field = $this->lexer->token->value;
} else {
$this->match(TokenType::T_IDENTIFIER);
$field .= '.' . $this->lexer->token->value;

$field = $this->lexer->token->value;

while ($this->lexer->isNextToken(TokenType::T_DOT)) {
$this->match(TokenType::T_DOT);
$this->match(TokenType::T_IDENTIFIER);
$field .= '.' . $this->lexer->token->value;
}
}
}

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

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

$this->match(TokenType::T_DOT);
$this->match(TokenType::T_MULTIPLY);

return new AST\AllFieldsExpression($identVariable);
}

/**
* SelectClause ::= "SELECT" ["DISTINCT"] SelectExpression {"," SelectExpression}
*/
Expand Down Expand Up @@ -1781,7 +1801,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);

Check failure on line 1804 in src/Query/Parser.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Parameter #1 $className of class Doctrine\ORM\Query\AST\NewObjectExpression constructor expects class-string, string given.

Check failure on line 1804 in src/Query/Parser.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Parameter #1 $className of class Doctrine\ORM\Query\AST\NewObjectExpression constructor expects class-string, string given.

// Defer NewObjectExpression validation
$this->deferredNewObjectExpressions[] = [
Expand Down Expand Up @@ -1828,7 +1848,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 Down Expand Up @@ -1934,10 +1954,14 @@ 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 ($token->value === '*') {
return $this->AllFieldsExpression();
}

if ($this->isMathOperator($peek)) {
return $this->SimpleArithmeticExpression();
}
Expand Down
71 changes: 60 additions & 11 deletions src/Query/SqlWalker.php
Original file line number Diff line number Diff line change
Expand Up @@ -1507,7 +1507,8 @@ public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesis
public function walkNewObject(AST\NewObjectExpression $newObjectExpression, string|null $newObjectResultAlias = null): string
{
$sqlSelectExpressions = [];
$objOwner = $objOwnerIdx = null;
$objIndex = $newObjectResultAlias ?: $this->newObjectCounter++;
$aliasGap = $newObjectExpression->hasNamedArgs ? null : 0;

if ($this->newObjectStack !== []) {
[$objOwner, $objOwnerIdx] = end($this->newObjectStack);
Expand All @@ -1517,9 +1518,14 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
}

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 @@ -1567,19 +1573,26 @@ public function walkNewObject(AST\NewObjectExpression $newObjectExpression, stri
$sqlSelectExpressions[] = trim($e->dispatch($this)) . ' AS ' . $columnAlias;
break;

case $e instanceof AST\AllFieldsExpression:
$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] = [
'className' => $newObjectExpression->className,
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];
$this->rsm->newObjectMappings[$columnAlias] = [
'className' => $newObjectExpression->className,
'objIndex' => $objIndex,
'argIndex' => $argIndex,
];
}
}

return implode(', ', $sqlSelectExpressions);
Expand Down Expand Up @@ -2282,6 +2295,42 @@ public function walkResultVariable(string $resultVariable): string
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()
: $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;

Check failure on line 2321 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Cannot assign new offset to list<string>|string.

Check failure on line 2321 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Cannot assign new offset to list<string>|string.

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

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

Check failure on line 2327 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (default, phpstan.neon)

Binary operation "+" between int|string and int results in an error.

Check failure on line 2327 in src/Query/SqlWalker.php

View workflow job for this annotation

GitHub Actions / Static Analysis with PHPStan (3.8.2, phpstan-dbal3.neon)

Binary operation "+" between int|string and int results in an error.
];
}

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

/**
* @return string The list in parentheses of valid child discriminators from the given class
*
Expand Down
22 changes: 22 additions & 0 deletions tests/Tests/Models/CMS/CmsDumbVariadicDTO.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\CMS;

class CmsDumbVariadicDTO
{
private array $values = [];

public function __construct(...$args)
{
foreach ($args as $key => $val) {
$this->values[$key] = $val;
}
}

public function __get(string $key): mixed
{
return $this->values[$key] ?? null;
}
}
Loading

0 comments on commit 6d9c4aa

Please sign in to comment.