fix(Query): keep all occurrences of a repeated identification variable

Parser::$identVariableExpressions was keyed by alias and held a single
SelectExpression per alias, so listing the same identification variable
twice in a SELECT clause made the second node overwrite the first.

fixIdentificationVariableOrder() only unsets and re-appends the nodes it
knows about, so the forgotten first node stayed at the head of the
select clause, ahead of its own parent. The hydrator then reached the
child before the parent existed in resultPointers, took the "parent
object of relation not found" branch in ObjectHydrator and unset the
'fetched' hint for the association. Because the hints are only rebuilt
in prepare(), the association stayed marked as not fetched for the rest
of the hydration run: the fetch join was silently dropped and every
parent's collection lazy loaded, while the query still returned correct
data.

Track every select expression per identification variable and move all
of them when reordering, so duplicates are re-appended together behind
their parent, in declaration order. With a single occurrence per alias
the inner loop runs once and the behaviour is unchanged.
This commit is contained in:
dualfroz committed 2026-09-03 13:30:28 +02:00
1 parent 8703a8e5de
commit 2c75209ce3
2 files changed
+36 -7

No files matched your search

+15 -7
View File
@@ -19,6 +19,7 @@ use ReflectionClass;
use function array_intersect;
use function array_key_exists;
use function array_keys;
use function array_search;
use function assert;
use function class_exists;
@@ -167,7 +168,13 @@ final class Parser
*/
private $customOutputWalker;
/** @phpstan-var array<string, AST\SelectExpression> */
/**
* All select expressions that refer to an identification variable, grouped
* by that variable. An alias may be listed more than once in the select
* clause, so every occurrence has to be tracked, not just the last one.
*
* @phpstan-var array<string, non-empty-list<AST\SelectExpression>>
*/
private array $identVariableExpressions = [];
/**
@@ -405,12 +412,13 @@ final class Parser
continue;
}
$expr = $this->identVariableExpressions[$dqlAlias];
$key = array_search($expr, $AST->selectClause->selectExpressions, true);
foreach ($this->identVariableExpressions[$dqlAlias] as $expr) {
$key = array_search($expr, $AST->selectClause->selectExpressions, true);
unset($AST->selectClause->selectExpressions[$key]);
unset($AST->selectClause->selectExpressions[$key]);
$AST->selectClause->selectExpressions[] = $expr;
$AST->selectClause->selectExpressions[] = $expr;
}
}
}
@@ -796,7 +804,7 @@ final class Parser
return;
}
foreach ($this->identVariableExpressions as $dqlAlias => $expr) {
foreach (array_keys($this->identVariableExpressions) as $dqlAlias) {
if (isset($this->queryComponents[$dqlAlias]) && ! isset($this->queryComponents[$dqlAlias]['parent'])) {
return;
}
@@ -2290,7 +2298,7 @@ final class Parser
$expr = new AST\SelectExpression($expression, $aliasResultVariable, $hiddenAliasResultVariable);
if ($identVariable) {
$this->identVariableExpressions[$identVariable] = $expr;
$this->identVariableExpressions[$identVariable][] = $expr;
}
return $expr;