From cfb978a3f074a4344b018619be611b04644ce93c Mon Sep 17 00:00:00 2001 From: podles Date: Wed, 28 Jan 2026 23:45:22 +0100 Subject: [PATCH 1/3] Fix false positive for aliases matching PHP class names When a QueryBuilder alias like 'event' coincidentally matches an existing PHP class name (e.g. \Event from ext-event stubs), isClassString() returns yes and isTransient() returns true, causing a DynamicQueryBuilderArgumentException. This made the query type resolve to mixed instead of the correct entity type. Invert the transient check so that only non-transient (actual entity) class-strings are resolved to their FQCN. Transient class-strings now fall through to constant scalar handling, where they are treated as plain string aliases. Co-Authored-By: Claude Opus 4.5 --- src/Type/Doctrine/ArgumentsProcessor.php | 11 ++++++----- .../data/QueryResult/queryBuilderGetQuery.php | 10 ++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index 2cd94fd7..58f967f1 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -61,12 +61,13 @@ public function processArgs( if ($value->isClassString()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { /** @var class-string $className */ $className = $value->getClassStringObjectType()->getObjectClassNames()[0]; - if ($this->objectMetadataResolver->isTransient($className)) { - throw new DynamicQueryBuilderArgumentException(); + if (!$this->objectMetadataResolver->isTransient($className)) { + $args[] = $className; + continue; } - - $args[] = $className; - continue; + // Transient class-string: fall through to constant scalar handling. + // This handles aliases like 'override' or 'event' that coincidentally + // match a PHP class name but are not intended as entity class references. } if (count($value->getConstantScalarValues()) !== 1) { diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php index c0cf3068..49a1b8a7 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderGetQuery.php @@ -310,4 +310,14 @@ public function testNonEntityClassString(EntityManagerInterface $em, string $cla assertType('mixed', $result); } + public function testEventAlias(EntityManagerInterface $em): void + { + $query = $em->createQueryBuilder() + ->select('event') + ->from(Many::class, 'event') + ->getQuery(); + + assertType('Doctrine\ORM\Query', $query); + } + } From e328dc94586eced4967e82f55392d1ffa2c0f25b Mon Sep 17 00:00:00 2001 From: podles Date: Thu, 29 Jan 2026 09:00:03 +0100 Subject: [PATCH 2/3] CR janedbal --- src/Type/Doctrine/ArgumentsProcessor.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index 58f967f1..c3e4fc74 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -7,6 +7,7 @@ use PHPStan\Rules\Doctrine\ORM\DynamicQueryBuilderArgumentException; use PHPStan\Type\Doctrine\QueryBuilder\Expr\ExprType; use function count; +use function in_array; use function strpos; /** @api */ @@ -32,7 +33,7 @@ public function processArgs( ): array { $args = []; - foreach ($methodCallArgs as $arg) { + foreach ($methodCallArgs as $argIndex => $arg) { if ($arg->unpack) { throw new DynamicQueryBuilderArgumentException(); } @@ -61,13 +62,14 @@ public function processArgs( if ($value->isClassString()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { /** @var class-string $className */ $className = $value->getClassStringObjectType()->getObjectClassNames()[0]; - if (!$this->objectMetadataResolver->isTransient($className)) { + $isEntityClassArgument = $argIndex === 0 && in_array($methodName, ['from', 'join', 'innerJoin', 'leftJoin', 'rightJoin'], true); + if ($isEntityClassArgument) { + if ($this->objectMetadataResolver->isTransient($className)) { + throw new DynamicQueryBuilderArgumentException(); + } $args[] = $className; continue; } - // Transient class-string: fall through to constant scalar handling. - // This handles aliases like 'override' or 'event' that coincidentally - // match a PHP class name but are not intended as entity class references. } if (count($value->getConstantScalarValues()) !== 1) { From 8f1d9bfec72afda527c55f085ad31130b145bd0c Mon Sep 17 00:00:00 2001 From: podles Date: Thu, 29 Jan 2026 09:44:37 +0100 Subject: [PATCH 3/3] CR: There is no rightJoin method on QB --- src/Type/Doctrine/ArgumentsProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Doctrine/ArgumentsProcessor.php b/src/Type/Doctrine/ArgumentsProcessor.php index c3e4fc74..bac4e508 100644 --- a/src/Type/Doctrine/ArgumentsProcessor.php +++ b/src/Type/Doctrine/ArgumentsProcessor.php @@ -62,7 +62,7 @@ public function processArgs( if ($value->isClassString()->yes() && count($value->getClassStringObjectType()->getObjectClassNames()) === 1) { /** @var class-string $className */ $className = $value->getClassStringObjectType()->getObjectClassNames()[0]; - $isEntityClassArgument = $argIndex === 0 && in_array($methodName, ['from', 'join', 'innerJoin', 'leftJoin', 'rightJoin'], true); + $isEntityClassArgument = $argIndex === 0 && in_array($methodName, ['from', 'join', 'innerJoin', 'leftJoin'], true); if ($isEntityClassArgument) { if ($this->objectMetadataResolver->isTransient($className)) { throw new DynamicQueryBuilderArgumentException();