From c52fb70682083c2a0f2237281d1e1b27774ca5ba Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Fri, 17 Jul 2026 11:57:32 +0200 Subject: [PATCH 1/2] Reuse the QueryBuilder ObjectType Create the QueryBuilder ObjectType once in the resolver constructor and reuse it for return-type compatibility checks. The resolver is called more than 600,000 times in the measured workload. The ObjectType is immutable for this use, so allocating the same value for each method candidate only adds allocation and CPU overhead. Benchmark: uncached Sylius analysis of 2,366 files with eight workers, five runs per variant. Median elapsed time fell from 28.91 s to 28.34 s (-2.0%) and aggregate user+sys CPU from 176.22 s to 169.68 s (-3.7%). --- .../ReturnQueryBuilderExpressionTypeResolverExtension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php index 5f308ba1..219b173d 100644 --- a/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php +++ b/src/Type/Doctrine/QueryBuilder/ReturnQueryBuilderExpressionTypeResolverExtension.php @@ -25,11 +25,14 @@ class ReturnQueryBuilderExpressionTypeResolverExtension implements ExpressionTyp private OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser; + private ObjectType $queryBuilderObjectType; + public function __construct( OtherMethodQueryBuilderParser $otherMethodQueryBuilderParser ) { $this->otherMethodQueryBuilderParser = $otherMethodQueryBuilderParser; + $this->queryBuilderObjectType = new ObjectType(QueryBuilder::class); } public function getType(Expr $expr, Scope $scope): ?Type @@ -50,7 +53,7 @@ public function getType(Expr $expr, Scope $scope): ?Type $returnType = ParametersAcceptorSelector::selectFromArgs($scope, $expr->getArgs(), $methodReflection->getVariants())->getReturnType(); - $returnsQueryBuilder = (new ObjectType(QueryBuilder::class))->isSuperTypeOf($returnType)->yes(); + $returnsQueryBuilder = $this->queryBuilderObjectType->isSuperTypeOf($returnType)->yes(); if (!$returnsQueryBuilder) { return null; From 7e82e8c85bfc38cce8cacb9e69233c41a94ca58a Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 23 Jul 2026 11:03:24 +0200 Subject: [PATCH 2/2] Test nullable QueryBuilder return types --- .../queryBuilderExpressionTypeResolver.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php index 2a6af1d9..e8a913a8 100644 --- a/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php +++ b/tests/Type/Doctrine/data/QueryResult/queryBuilderExpressionTypeResolver.php @@ -66,6 +66,11 @@ public function testFirstClassCallableDoesNotFail(EntityManagerInterface $em): v $this->getQueryBuilder(...); } + public function testNullableQueryBuilderIsNotInferred(EntityManagerInterface $em): void + { + assertType('Doctrine\\ORM\\QueryBuilder|null', $this->getNullableQueryBuilder($em)); + } + private function adjustQueryBuilderToIndexByInt(QueryBuilder $qb): void { $qb->indexBy('m', 'm.intColumn'); @@ -100,6 +105,13 @@ private static function getStaticQueryBuilder(EntityManagerInterface $em): Query ->from(Many::class, 'm'); } + private function getNullableQueryBuilder(EntityManagerInterface $em): ?QueryBuilder + { + return $em->createQueryBuilder() + ->select('m') + ->from(Many::class, 'm'); + } + private function getBranchingQueryBuilder(EntityManagerInterface $em): QueryBuilder { $queryBuilder = $em->createQueryBuilder()